diff --git a/HISTORY.md b/HISTORY.md index 86a7249c..3d99c305 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,32 @@ http://visjs.org +## not yet released, version 4.0.0-SNAPSHOT + +### General + +- Changed the build scripts to include a transpilation of ES6 to ES5 + (using http://6to5.org), so we can use ES6 features in the vis.js code. + When creating a custom bundle using browserify, one now needs to add a + transform step using `6to5ify`, this is described in README.md. + + +### Timeline + +- Fixed range items not being displayed smaller than 10 pixels (twice the + padding). In order to have overflowing text, one should now apply css style + `.vis.timeline .item.range { overflow: visible; }` instead of + `.vis.timeline .item.range .content { overflow: visible; }`. + See example 18_range_overflow.html. +- Fixed invalid css names for time axis grid, renamed hours class names from + `4-8h` to `h4-h8`. + +### Network + +- Rebuilt the cluster system + + + ## not yet released, version 3.10.1-SNAPSHOT ### Network @@ -65,6 +91,11 @@ http://visjs.org - Fixed a bug in the `DataSet` returning an empty object instead of `null` when no item was found when using both a filter and specifying fields. +### Timeline + +- Implemented option `timeAxis: {scale: string, step: number}` to set a + fixed scale. + ## 2015-01-16, version 3.9.1 diff --git a/README.md b/README.md index 92f11797..2d397abb 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ The source code of vis.js consists of commonjs modules, which makes it possible Before you can do a build: -- Install node.js, npm, and browserify on your system. +- Install node.js, npm, browserify, and uglify-js on your system. - Download or clone the vis.js project. - Install the dependencies of vis.js by running `npm install` in the root of the project. @@ -179,7 +179,7 @@ exports.Timeline = require('./lib/timeline/Timeline'); Install browserify globally via `[sudo] npm install -g browserify`, then create a custom bundle like: - browserify custom.js -o vis-custom.js -s vis + browserify custom.js -t 6to5ify -o vis-custom.js -s vis This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis` containing only `DataSet` and `Timeline`. The generated bundle can be minified with uglifyjs (installed globally with `[sudo] npm install -g uglify-js`): @@ -204,7 +204,7 @@ The custom bundle can now be loaded like: The default bundle `vis.js` is standalone and includes external dependencies such as hammer.js and moment.js. When these libraries are already loaded by the application, vis.js does not need to include these dependencies itself too. To build a custom bundle of vis.js excluding moment.js and hammer.js, run browserify in the root of the project: - browserify index.js -o vis-custom.js -s vis -x moment -x hammerjs + browserify index.js -t 6to5ify -o vis-custom.js -s vis -x moment -x hammerjs This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis`, and has moment and hammerjs excluded. The generated bundle can be minified with uglifyjs: diff --git a/bower.json b/bower.json index c65da124..7d53e2ad 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "3.10.1-SNAPSHOT", + "version": "4.0.0-SNAPSHOT", "main": ["dist/vis.min.js", "dist/vis.min.css"], "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", diff --git a/dist/vis.css b/dist/vis.css index 19e61cad..0b7a13ed 100644 --- a/dist/vis.css +++ b/dist/vis.css @@ -173,7 +173,7 @@ border-width: 1px; background-color: #D5DDF6; display: inline-block; - padding: 5px; + overflow: hidden; } .vis.timeline .item.selected { @@ -217,7 +217,6 @@ } .vis.timeline .item.background { - overflow: hidden; border: none; background-color: rgba(213, 221, 246, 0.4); box-sizing: border-box; @@ -229,13 +228,11 @@ position: relative; display: inline-block; max-width: 100%; - overflow: hidden; } .vis.timeline .item.background .content { position: absolute; display: inline-block; - overflow: hidden; max-width: 100%; margin: 5px; } @@ -250,7 +247,8 @@ .vis.timeline .item .content { white-space: nowrap; - overflow: hidden; + box-sizing: border-box; + padding: 5px; } .vis.timeline .item .delete { diff --git a/dist/vis.js b/dist/vis.js index 9ce576c2..c99c601e 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -4,8 +4,8 @@ * * A dynamic, browser-based visualization library. * - * @version 3.10.1-SNAPSHOT - * @date 2015-02-26 + * @version 4.0.0-SNAPSHOT + * @date 2015-02-27 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -81,6 +81,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 0 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // utils exports.util = __webpack_require__(1); exports.DOMutil = __webpack_require__(6); @@ -148,18 +150,19 @@ return /******/ (function(modules) { // webpackBootstrap // Deprecated since v3.0.0 exports.Graph = function () { - throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)'); + throw new Error("Graph is renamed to Network. Please create a graph as new vis.Network(...)"); }; // bundled external libraries exports.moment = __webpack_require__(2); exports.hammer = __webpack_require__(19); - /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // utility functions // first check if moment.js is already loaded in the browser window, if so, @@ -171,8 +174,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {*} object * @return {Boolean} isNumber */ - exports.isNumber = function(object) { - return (object instanceof Number || typeof object == 'number'); + exports.isNumber = function (object) { + return object instanceof Number || typeof object == "number"; }; @@ -185,23 +188,22 @@ return /******/ (function(modules) { // webpackBootstrap * @param value * @returns {number} */ - exports.giveRange = function(min,max,total,value) { + exports.giveRange = function (min, max, total, value) { if (max == min) { return 0.5; - } - else { + } else { var scale = 1 / (max - min); - return Math.max(0,(value - min)*scale); + return Math.max(0, (value - min) * scale); } - } + }; /** * Test whether given object is a string * @param {*} object * @return {Boolean} isString */ - exports.isString = function(object) { - return (object instanceof String || typeof object == 'string'); + exports.isString = function (object) { + return object instanceof String || typeof object == "string"; }; /** @@ -209,17 +211,15 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Date | String} object * @return {Boolean} isDate */ - exports.isDate = function(object) { + exports.isDate = function (object) { if (object instanceof Date) { return true; - } - else if (exports.isString(object)) { + } else if (exports.isString(object)) { // test whether this string contains a date var match = ASPDateRegex.exec(object); if (match) { return true; - } - else if (!isNaN(Date.parse(object))) { + } else if (!isNaN(Date.parse(object))) { return true; } } @@ -232,11 +232,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {*} object * @return {Boolean} isDataTable */ - exports.isDataTable = function(object) { - return (typeof (google) !== 'undefined') && - (google.visualization) && - (google.visualization.DataTable) && - (object instanceof google.visualization.DataTable); + exports.isDataTable = function (object) { + return typeof google !== "undefined" && google.visualization && google.visualization.DataTable && object instanceof google.visualization.DataTable; }; /** @@ -244,20 +241,13 @@ return /******/ (function(modules) { // webpackBootstrap * source: http://stackoverflow.com/a/105074/1262753 * @return {String} uuid */ - exports.randomUUID = function() { + exports.randomUUID = function () { var S4 = function () { - return Math.floor( - Math.random() * 0x10000 /* 65536 */ + return Math.floor(Math.random() * 65536 /* 65536 */ ).toString(16); }; - return ( - S4() + S4() + '-' + - S4() + '-' + - S4() + '-' + - S4() + '-' + - S4() + S4() + S4() - ); + return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4(); }; /** @@ -290,7 +280,7 @@ return /******/ (function(modules) { // webpackBootstrap */ exports.selectiveExtend = function (props, a, b) { if (!Array.isArray(props)) { - throw new Error('Array with property names expected as first argument'); + throw new Error("Array with property names expected as first argument"); } for (var i = 2; i < arguments.length; i++) { @@ -317,7 +307,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.selectiveDeepExtend = function (props, a, b) { // TODO: add support for Arrays to deepExtend if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); + throw new TypeError("Arrays are not supported by deepExtend"); } for (var i = 2; i < arguments.length; i++) { var other = arguments[i]; @@ -330,16 +320,14 @@ return /******/ (function(modules) { // webpackBootstrap } if (a[prop].constructor === Object) { exports.deepExtend(a[prop], b[prop]); - } - else { + } else { a[prop] = b[prop]; } } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); + throw new TypeError("Arrays are not supported by deepExtend"); } else { a[prop] = b[prop]; } - } } } @@ -357,7 +345,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.selectiveNotDeepExtend = function (props, a, b) { // TODO: add support for Arrays to deepExtend if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); + throw new TypeError("Arrays are not supported by deepExtend"); } for (var prop in b) { if (b.hasOwnProperty(prop)) { @@ -368,12 +356,11 @@ return /******/ (function(modules) { // webpackBootstrap } if (a[prop].constructor === Object) { exports.deepExtend(a[prop], b[prop]); - } - else { + } else { a[prop] = b[prop]; } } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); + throw new TypeError("Arrays are not supported by deepExtend"); } else { a[prop] = b[prop]; } @@ -387,28 +374,29 @@ return /******/ (function(modules) { // webpackBootstrap * Deep extend an object a with the properties of object b * @param {Object} a * @param {Object} b + * @param {Boolean} protoExtend --> optional parameter. If true, the prototype values will also be extended. + * (ie. the options objects that inherit from others will also get the inherited options) * @returns {Object} */ - exports.deepExtend = function(a, b) { + exports.deepExtend = function (a, b, protoExtend) { // TODO: add support for Arrays to deepExtend if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); + throw new TypeError("Arrays are not supported by deepExtend"); } for (var prop in b) { - if (b.hasOwnProperty(prop)) { + if (b.hasOwnProperty(prop) || protoExtend === true) { if (b[prop] && b[prop].constructor === Object) { if (a[prop] === undefined) { a[prop] = {}; } if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop]); - } - else { + exports.deepExtend(a[prop], b[prop], protoExtend); + } else { a[prop] = b[prop]; } } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); + throw new TypeError("Arrays are not supported by deepExtend"); } else { a[prop] = b[prop]; } @@ -443,7 +431,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {*} object * @throws Error */ - exports.convert = function(object, type) { + exports.convert = function (object, type) { var match; if (object === undefined) { @@ -456,32 +444,31 @@ return /******/ (function(modules) { // webpackBootstrap if (!type) { return object; } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); + if (!(typeof type === "string") && !(type instanceof String)) { + throw new Error("Type must be a string"); } //noinspection FallthroughInSwitchStatementJS switch (type) { - case 'boolean': - case 'Boolean': + case "boolean": + case "Boolean": return Boolean(object); - case 'number': - case 'Number': + case "number": + case "Number": return Number(object.valueOf()); - case 'string': - case 'String': + case "string": + case "String": return String(object); - case 'Date': + case "Date": if (exports.isNumber(object)) { return new Date(object); } if (object instanceof Date) { return new Date(object.valueOf()); - } - else if (moment.isMoment(object)) { + } else if (moment.isMoment(object)) { return new Date(object.valueOf()); } if (exports.isString(object)) { @@ -489,25 +476,20 @@ return /******/ (function(modules) { // webpackBootstrap if (match) { // object is an ASP date return new Date(Number(match[1])); // parse number - } - else { + } else { return moment(object).toDate(); // parse string } - } - else { - throw new Error( - 'Cannot convert object of type ' + exports.getType(object) + - ' to type Date'); + } else { + throw new Error("Cannot convert object of type " + exports.getType(object) + " to type Date"); } - case 'Moment': + case "Moment": if (exports.isNumber(object)) { return moment(object); } if (object instanceof Date) { return moment(object.valueOf()); - } - else if (moment.isMoment(object)) { + } else if (moment.isMoment(object)) { return moment(object); } if (exports.isString(object)) { @@ -515,70 +497,53 @@ return /******/ (function(modules) { // webpackBootstrap if (match) { // object is an ASP date return moment(Number(match[1])); // parse number - } - else { + } else { return moment(object); // parse string } - } - else { - throw new Error( - 'Cannot convert object of type ' + exports.getType(object) + - ' to type Date'); + } else { + throw new Error("Cannot convert object of type " + exports.getType(object) + " to type Date"); } - case 'ISODate': + case "ISODate": if (exports.isNumber(object)) { return new Date(object); - } - else if (object instanceof Date) { + } else if (object instanceof Date) { return object.toISOString(); - } - else if (moment.isMoment(object)) { + } else if (moment.isMoment(object)) { return object.toDate().toISOString(); - } - else if (exports.isString(object)) { + } else if (exports.isString(object)) { match = ASPDateRegex.exec(object); if (match) { // object is an ASP date return new Date(Number(match[1])).toISOString(); // parse number - } - else { + } else { return new Date(object).toISOString(); // parse string } - } - else { - throw new Error( - 'Cannot convert object of type ' + exports.getType(object) + - ' to type ISODate'); + } else { + throw new Error("Cannot convert object of type " + exports.getType(object) + " to type ISODate"); } - case 'ASPDate': + case "ASPDate": if (exports.isNumber(object)) { - return '/Date(' + object + ')/'; - } - else if (object instanceof Date) { - return '/Date(' + object.valueOf() + ')/'; - } - else if (exports.isString(object)) { + return "/Date(" + object + ")/"; + } else if (object instanceof Date) { + return "/Date(" + object.valueOf() + ")/"; + } else if (exports.isString(object)) { match = ASPDateRegex.exec(object); var value; if (match) { // object is an ASP date value = new Date(Number(match[1])).valueOf(); // parse number - } - else { + } else { value = new Date(object).valueOf(); // parse string } - return '/Date(' + value + ')/'; - } - else { - throw new Error( - 'Cannot convert object of type ' + exports.getType(object) + - ' to type ASPDate'); + return "/Date(" + value + ")/"; + } else { + throw new Error("Cannot convert object of type " + exports.getType(object) + " to type ASPDate"); } default: - throw new Error('Unknown type "' + type + '"'); + throw new Error("Unknown type \"" + type + "\""); } }; @@ -592,38 +557,35 @@ return /******/ (function(modules) { // webpackBootstrap * @param {*} object * @return {String} type */ - exports.getType = function(object) { + exports.getType = function (object) { var type = typeof object; - if (type == 'object') { + if (type == "object") { if (object == null) { - return 'null'; + return "null"; } if (object instanceof Boolean) { - return 'Boolean'; + return "Boolean"; } if (object instanceof Number) { - return 'Number'; + return "Number"; } if (object instanceof String) { - return 'String'; + return "String"; } if (Array.isArray(object)) { - return 'Array'; + return "Array"; } if (object instanceof Date) { - return 'Date'; + return "Date"; } - return 'Object'; - } - else if (type == 'number') { - return 'Number'; - } - else if (type == 'boolean') { - return 'Boolean'; - } - else if (type == 'string') { - return 'String'; + return "Object"; + } else if (type == "number") { + return "Number"; + } else if (type == "boolean") { + return "Boolean"; + } else if (type == "string") { + return "String"; } return type; @@ -635,7 +597,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {number} left The absolute left position of this element * in the browser page. */ - exports.getAbsoluteLeft = function(elem) { + exports.getAbsoluteLeft = function (elem) { return elem.getBoundingClientRect().left + window.pageXOffset; }; @@ -645,7 +607,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {number} top The absolute top position of this element * in the browser page. */ - exports.getAbsoluteTop = function(elem) { + exports.getAbsoluteTop = function (elem) { return elem.getBoundingClientRect().top + window.pageYOffset; }; @@ -654,11 +616,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Element} elem * @param {String} className */ - exports.addClassName = function(elem, className) { - var classes = elem.className.split(' '); + exports.addClassName = function (elem, className) { + var classes = elem.className.split(" "); if (classes.indexOf(className) == -1) { classes.push(className); // add the class to the array - elem.className = classes.join(' '); + elem.className = classes.join(" "); } }; @@ -667,12 +629,12 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Element} elem * @param {String} className */ - exports.removeClassName = function(elem, className) { - var classes = elem.className.split(' '); + exports.removeClassName = function (elem, className) { + var classes = elem.className.split(" "); var index = classes.indexOf(className); if (index != -1) { classes.splice(index, 1); // remove the class from the array - elem.className = classes.join(' '); + elem.className = classes.join(" "); } }; @@ -685,16 +647,14 @@ return /******/ (function(modules) { // webpackBootstrap * the object or array with three parameters: * callback(value, index, object) */ - exports.forEach = function(object, callback) { - var i, - len; + exports.forEach = function (object, callback) { + var i, len; if (Array.isArray(object)) { // array for (i = 0, len = object.length; i < len; i++) { callback(object[i], i, object); } - } - else { + } else { // object for (i in object) { if (object.hasOwnProperty(i)) { @@ -710,7 +670,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} object * @param {Array} array */ - exports.toArray = function(object) { + exports.toArray = function (object) { var array = []; for (var prop in object) { @@ -718,7 +678,7 @@ return /******/ (function(modules) { // webpackBootstrap } return array; - } + }; /** * Update a property in an object @@ -727,12 +687,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {*} value * @return {Boolean} changed */ - exports.updateProperty = function(object, key, value) { + exports.updateProperty = function (object, key, value) { if (object[key] !== value) { object[key] = value; return true; - } - else { + } else { return false; } }; @@ -745,18 +704,17 @@ return /******/ (function(modules) { // webpackBootstrap * @param {function} listener The callback function to be executed * @param {boolean} [useCapture] */ - exports.addEventListener = function(element, action, listener, useCapture) { + exports.addEventListener = function (element, action, listener, useCapture) { if (element.addEventListener) { - if (useCapture === undefined) - useCapture = false; + if (useCapture === undefined) useCapture = false; if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox + action = "DOMMouseScroll"; // For Firefox } element.addEventListener(action, listener, useCapture); } else { - element.attachEvent("on" + action, listener); // IE browsers + element.attachEvent("on" + action, listener); // IE browsers } }; @@ -767,14 +725,13 @@ return /******/ (function(modules) { // webpackBootstrap * @param {function} listener The listener function * @param {boolean} [useCapture] */ - exports.removeEventListener = function(element, action, listener, useCapture) { + exports.removeEventListener = function (element, action, listener, useCapture) { if (element.removeEventListener) { // non-IE browsers - if (useCapture === undefined) - useCapture = false; + if (useCapture === undefined) useCapture = false; if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox + action = "DOMMouseScroll"; // For Firefox } element.removeEventListener(action, listener, useCapture); @@ -788,14 +745,12 @@ return /******/ (function(modules) { // webpackBootstrap * Cancels the event if it is cancelable, without stopping further propagation of the event. */ exports.preventDefault = function (event) { - if (!event) - event = window.event; + if (!event) event = window.event; if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } - else { - event.returnValue = false; // IE browsers + event.preventDefault(); // non-IE browsers + } else { + event.returnValue = false; // IE browsers } }; @@ -804,7 +759,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @return {Element} target element */ - exports.getTarget = function(event) { + exports.getTarget = function (event) { // code from http://www.quirksmode.org/js/events_properties.html if (!event) { event = window.event; @@ -814,8 +769,7 @@ return /******/ (function(modules) { // webpackBootstrap if (event.target) { target = event.target; - } - else if (event.srcElement) { + } else if (event.srcElement) { target = event.srcElement; } @@ -836,12 +790,12 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {Boolean} bool */ exports.option.asBoolean = function (value, defaultValue) { - if (typeof value == 'function') { + if (typeof value == "function") { value = value(); } if (value != null) { - return (value != false); + return value != false; } return defaultValue || null; @@ -854,7 +808,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {Number} number */ exports.option.asNumber = function (value, defaultValue) { - if (typeof value == 'function') { + if (typeof value == "function") { value = value(); } @@ -872,7 +826,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {String} str */ exports.option.asString = function (value, defaultValue) { - if (typeof value == 'function') { + if (typeof value == "function") { value = value(); } @@ -890,17 +844,15 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {String} size */ exports.option.asSize = function (value, defaultValue) { - if (typeof value == 'function') { + if (typeof value == "function") { value = value(); } if (exports.isString(value)) { return value; - } - else if (exports.isNumber(value)) { - return value + 'px'; - } - else { + } else if (exports.isNumber(value)) { + return value + "px"; + } else { return defaultValue || null; } }; @@ -912,7 +864,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {HTMLElement | null} dom */ exports.option.asElement = function (value, defaultValue) { - if (typeof value == 'function') { + if (typeof value == "function") { value = value(); } @@ -925,17 +877,17 @@ return /******/ (function(modules) { // webpackBootstrap * @param {String} hex * @returns {{r: *, g: *, b: *}} | 255 range */ - exports.hexToRGB = function(hex) { + exports.hexToRGB = function (hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function(m, r, g, b) { - return r + r + g + g + b + b; + hex = hex.replace(shorthandRegex, function (m, r, g, b) { + return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) } : null; }; @@ -945,21 +897,19 @@ return /******/ (function(modules) { // webpackBootstrap * @param opacity * @returns {*} */ - exports.overrideOpacity = function(color,opacity) { + exports.overrideOpacity = function (color, opacity) { if (color.indexOf("rgb") != -1) { - var rgb = color.substr(color.indexOf("(")+1).replace(")","").split(","); - return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + opacity + ")" - } - else { + var rgb = color.substr(color.indexOf("(") + 1).replace(")", "").split(","); + return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + opacity + ")"; + } else { var rgb = exports.hexToRGB(color); if (rgb == null) { return color; - } - else { - return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + opacity + ")" + } else { + return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + opacity + ")"; } } - } + }; /** * @@ -969,7 +919,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {string} * @constructor */ - exports.RGBToHex = function(red,green,blue) { + exports.RGBToHex = function (red, green, blue) { return "#" + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); }; @@ -979,60 +929,57 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object | String} color * @return {Object} colorObject */ - exports.parseColor = function(color) { + exports.parseColor = function (color) { var c; if (exports.isString(color)) { if (exports.isValidRGB(color)) { - var rgb = color.substr(4).substr(0,color.length-5).split(','); - color = exports.RGBToHex(rgb[0],rgb[1],rgb[2]); + var rgb = color.substr(4).substr(0, color.length - 5).split(","); + color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]); } if (exports.isValidHex(color)) { var hsv = exports.hexToHSV(color); - var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; - var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; - var darkerColorHex = exports.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v); - var lighterColorHex = exports.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v); + var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.45, v: Math.min(1, hsv.v * 1.05) }; + var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.v * 1.25), v: hsv.v * 0.6 }; + var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.h, darkerColorHSV.v); + var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v); c = { background: color, - border:darkerColorHex, + border: darkerColorHex, highlight: { - background:lighterColorHex, - border:darkerColorHex + background: lighterColorHex, + border: darkerColorHex }, hover: { - background:lighterColorHex, - border:darkerColorHex + background: lighterColorHex, + border: darkerColorHex } }; - } - else { + } else { c = { - background:color, - border:color, + background: color, + border: color, highlight: { - background:color, - border:color + background: color, + border: color }, hover: { - background:color, - border:color + background: color, + border: color } }; } - } - else { + } else { c = {}; - c.background = color.background || 'white'; + c.background = color.background || "white"; c.border = color.border || c.background; if (exports.isString(color.highlight)) { c.highlight = { border: color.highlight, background: color.highlight - } - } - else { + }; + } else { c.highlight = {}; c.highlight.background = color.highlight && color.highlight.background || c.background; c.highlight.border = color.highlight && color.highlight.border || c.border; @@ -1042,9 +989,8 @@ return /******/ (function(modules) { // webpackBootstrap c.hover = { border: color.hover, background: color.hover - } - } - else { + }; + } else { c.hover = {}; c.hover.background = color.hover && color.hover.background || c.background; c.hover.border = color.hover && color.hover.border || c.border; @@ -1063,23 +1009,23 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {*} * @constructor */ - exports.RGBToHSV = function(red,green,blue) { - red=red/255; green=green/255; blue=blue/255; - var minRGB = Math.min(red,Math.min(green,blue)); - var maxRGB = Math.max(red,Math.max(green,blue)); + exports.RGBToHSV = function (red, green, blue) { + red = red / 255;green = green / 255;blue = blue / 255; + var minRGB = Math.min(red, Math.min(green, blue)); + var maxRGB = Math.max(red, Math.max(green, blue)); // Black-gray-white if (minRGB == maxRGB) { - return {h:0,s:0,v:minRGB}; + return { h: 0, s: 0, v: minRGB }; } // Colors other than black-gray-white: - var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red); - var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5); - var hue = 60*(h - d/(maxRGB - minRGB))/360; - var saturation = (maxRGB - minRGB)/maxRGB; + var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red; + var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5; + var hue = 60 * (h - d / (maxRGB - minRGB)) / 360; + var saturation = (maxRGB - minRGB) / maxRGB; var value = maxRGB; - return {h:hue,s:saturation,v:value}; + return { h: hue, s: saturation, v: value }; }; var cssUtil = { @@ -1087,9 +1033,9 @@ return /******/ (function(modules) { // webpackBootstrap split: function (cssText) { var styles = {}; - cssText.split(';').forEach(function (style) { - if (style.trim() != '') { - var parts = style.split(':'); + cssText.split(";").forEach(function (style) { + if (style.trim() != "") { + var parts = style.split(":"); var key = parts[0].trim(); var value = parts[1].trim(); styles[key] = value; @@ -1101,11 +1047,9 @@ return /******/ (function(modules) { // webpackBootstrap // build a css text string from an object with key/values join: function (styles) { - return Object.keys(styles) - .map(function (key) { - return key + ': ' + styles[key]; - }) - .join('; '); + return Object.keys(styles).map(function (key) { + return key + ": " + styles[key]; + }).join("; "); } }; @@ -1148,7 +1092,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {{r: number, g: number, b: number}} * @constructor */ - exports.HSVToRGB = function(h, s, v) { + exports.HSVToRGB = function (h, s, v) { var r, g, b; var i = Math.floor(h * 6); @@ -1158,37 +1102,43 @@ return /******/ (function(modules) { // webpackBootstrap var t = v * (1 - (1 - f) * s); switch (i % 6) { - case 0: r = v, g = t, b = p; break; - case 1: r = q, g = v, b = p; break; - case 2: r = p, g = v, b = t; break; - case 3: r = p, g = q, b = v; break; - case 4: r = t, g = p, b = v; break; - case 5: r = v, g = p, b = q; break; + case 0: + r = v, g = t, b = p;break; + case 1: + r = q, g = v, b = p;break; + case 2: + r = p, g = v, b = t;break; + case 3: + r = p, g = q, b = v;break; + case 4: + r = t, g = p, b = v;break; + case 5: + r = v, g = p, b = q;break; } - return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) }; + return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; }; - exports.HSVToHex = function(h, s, v) { + exports.HSVToHex = function (h, s, v) { var rgb = exports.HSVToRGB(h, s, v); return exports.RGBToHex(rgb.r, rgb.g, rgb.b); }; - exports.hexToHSV = function(hex) { + exports.hexToHSV = function (hex) { var rgb = exports.hexToRGB(hex); return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); }; - exports.isValidHex = function(hex) { + exports.isValidHex = function (hex) { var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); return isOk; }; - exports.isValidRGB = function(rgb) { - rgb = rgb.replace(" ",""); + exports.isValidRGB = function (rgb) { + rgb = rgb.replace(" ", ""); var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb); return isOk; - } + }; /** * This recursively redirects the prototype of JSON objects to the referenceObject @@ -1197,7 +1147,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param referenceObject * @returns {*} */ - exports.selectiveBridgeObject = function(fields, referenceObject) { + exports.selectiveBridgeObject = function (fields, referenceObject) { if (typeof referenceObject == "object") { var objectTo = Object.create(referenceObject); for (var i = 0; i < fields.length; i++) { @@ -1208,8 +1158,7 @@ return /******/ (function(modules) { // webpackBootstrap } } return objectTo; - } - else { + } else { return null; } }; @@ -1221,7 +1170,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param referenceObject * @returns {*} */ - exports.bridgeObject = function(referenceObject) { + exports.bridgeObject = function (referenceObject) { if (typeof referenceObject == "object") { var objectTo = Object.create(referenceObject); for (var i in referenceObject) { @@ -1232,8 +1181,7 @@ return /******/ (function(modules) { // webpackBootstrap } } return objectTo; - } - else { + } else { return null; } }; @@ -1250,10 +1198,9 @@ return /******/ (function(modules) { // webpackBootstrap */ exports.mergeOptions = function (mergeTarget, options, option) { if (options[option] !== undefined) { - if (typeof options[option] == 'boolean') { + if (typeof options[option] == "boolean") { mergeTarget[option].enabled = options[option]; - } - else { + } else { mergeTarget[option].enabled = true; for (var prop in options[option]) { if (options[option].hasOwnProperty(prop)) { @@ -1262,7 +1209,7 @@ return /******/ (function(modules) { // webpackBootstrap } } } - } + }; /** @@ -1276,7 +1223,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {number} * @private */ - exports.binarySearchCustom = function(orderedItems, searchFunction, field, field2) { + exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { var maxIterations = 10000; var iteration = 0; var low = 0; @@ -1286,16 +1233,17 @@ return /******/ (function(modules) { // webpackBootstrap var middle = Math.floor((low + high) / 2); var item = orderedItems[middle]; - var value = (field2 === undefined) ? item[field] : item[field][field2]; + var value = field2 === undefined ? item[field] : item[field][field2]; var searchResult = searchFunction(value); - if (searchResult == 0) { // jihaa, found a visible item! + if (searchResult == 0) { + // jihaa, found a visible item! return middle; - } - else if (searchResult == -1) { // it is too small --> increase low + } else if (searchResult == -1) { + // it is too small --> increase low low = middle + 1; - } - else { // it is too big --> decrease high + } else { + // it is too big --> decrease high high = middle - 1; } @@ -1317,7 +1265,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {number} * @private */ - exports.binarySearchValue = function(orderedItems, target, field, sidePreference) { + exports.binarySearchValue = function (orderedItems, target, field, sidePreference) { var maxIterations = 10000; var iteration = 0; var low = 0; @@ -1326,25 +1274,27 @@ return /******/ (function(modules) { // webpackBootstrap while (low <= high && iteration < maxIterations) { // get a new guess - middle = Math.floor(0.5*(high+low)); - prevValue = orderedItems[Math.max(0,middle - 1)][field]; - value = orderedItems[middle][field]; - nextValue = orderedItems[Math.min(orderedItems.length-1,middle + 1)][field]; + middle = Math.floor(0.5 * (high + low)); + prevValue = orderedItems[Math.max(0, middle - 1)][field]; + value = orderedItems[middle][field]; + nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; - if (value == target) { // we found the target + if (value == target) { + // we found the target return middle; - } - else if (prevValue < target && value > target) { // target is in between of the previous and the current - return sidePreference == 'before' ? Math.max(0,middle - 1) : middle; - } - else if (value < target && nextValue > target) { // target is in between of the current and the next - return sidePreference == 'before' ? middle : Math.min(orderedItems.length-1,middle + 1); - } - else { // didnt find the target, we need to change our boundaries. - if (value < target) { // it is too small --> increase low + } else if (prevValue < target && value > target) { + // target is in between of the previous and the current + return sidePreference == "before" ? Math.max(0, middle - 1) : middle; + } else if (value < target && nextValue > target) { + // target is in between of the current and the next + return sidePreference == "before" ? middle : Math.min(orderedItems.length - 1, middle + 1); + } else { + // didnt find the target, we need to change our boundaries. + if (value < target) { + // it is too small --> increase low low = middle + 1; - } - else { // it is too big --> decrease high + } else { + // it is too big --> decrease high high = middle - 1; } } @@ -1366,10 +1316,10 @@ return /******/ (function(modules) { // webpackBootstrap */ exports.easeInOutQuad = function (t, start, end, duration) { var change = end - start; - t /= duration/2; - if (t < 1) return change/2*t*t + start; + t /= duration / 2; + if (t < 1) return change / 2 * t * t + start; t--; - return -change/2 * (t*(t-2) - 1) + start; + return -change / 2 * (t * (t - 2) - 1) + start; }; @@ -1382,55 +1332,55 @@ return /******/ (function(modules) { // webpackBootstrap exports.easingFunctions = { // no easing, no acceleration linear: function (t) { - return t + return t; }, // accelerating from zero velocity easeInQuad: function (t) { - return t * t + return t * t; }, // decelerating to zero velocity easeOutQuad: function (t) { - return t * (2 - t) + return t * (2 - t); }, // acceleration until halfway, then deceleration easeInOutQuad: function (t) { - return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }, // accelerating from zero velocity easeInCubic: function (t) { - return t * t * t + return t * t * t; }, // decelerating to zero velocity easeOutCubic: function (t) { - return (--t) * t * t + 1 + return --t * t * t + 1; }, // acceleration until halfway, then deceleration easeInOutCubic: function (t) { - return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 + return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; }, // accelerating from zero velocity easeInQuart: function (t) { - return t * t * t * t + return t * t * t * t; }, // decelerating to zero velocity easeOutQuart: function (t) { - return 1 - (--t) * t * t * t + return 1 - --t * t * t * t; }, // acceleration until halfway, then deceleration easeInOutQuart: function (t) { - return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t + return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; }, // accelerating from zero velocity easeInQuint: function (t) { - return t * t * t * t * t + return t * t * t * t * t; }, // decelerating to zero velocity easeOutQuint: function (t) { - return 1 + (--t) * t * t * t * t + return 1 + --t * t * t * t * t; }, // acceleration until halfway, then deceleration easeInOutQuint: function (t) { - return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t + return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; } }; @@ -1438,10 +1388,11 @@ return /******/ (function(modules) { // webpackBootstrap /* 2 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // first check if moment.js is already loaded in the browser window, if so, // use this instance. Else, load via commonjs. - module.exports = (typeof window !== 'undefined') && window['moment'] || __webpack_require__(3); - + module.exports = typeof window !== "undefined" && window.moment || __webpack_require__(3); /***/ }, /* 3 */ @@ -4526,6 +4477,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 6 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // DOM utility methods /** @@ -4533,7 +4486,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param JSONcontainer * @private */ - exports.prepareElements = function(JSONcontainer) { + exports.prepareElements = function (JSONcontainer) { // cleanup the redundant svgElements; for (var elementType in JSONcontainer) { if (JSONcontainer.hasOwnProperty(elementType)) { @@ -4550,7 +4503,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param JSONcontainer * @private */ - exports.cleanupElements = function(JSONcontainer) { + exports.cleanupElements = function (JSONcontainer) { // cleanup the redundant svgElements; for (var elementType in JSONcontainer) { if (JSONcontainer.hasOwnProperty(elementType)) { @@ -4577,22 +4530,21 @@ return /******/ (function(modules) { // webpackBootstrap exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) { var element; // allocate SVG element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before // check if there is an redundant element if (JSONcontainer[elementType].redundant.length > 0) { element = JSONcontainer[elementType].redundant[0]; JSONcontainer[elementType].redundant.shift(); - } - else { + } else { // create a new element and add it to the SVG - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); + element = document.createElementNS("http://www.w3.org/2000/svg", elementType); svgContainer.appendChild(element); } - } - else { + } else { // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); - JSONcontainer[elementType] = {used: [], redundant: []}; + element = document.createElementNS("http://www.w3.org/2000/svg", elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; svgContainer.appendChild(element); } JSONcontainer[elementType].used.push(element); @@ -4613,31 +4565,28 @@ return /******/ (function(modules) { // webpackBootstrap exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { var element; // allocate DOM element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before // check if there is an redundant element if (JSONcontainer[elementType].redundant.length > 0) { element = JSONcontainer[elementType].redundant[0]; JSONcontainer[elementType].redundant.shift(); - } - else { + } else { // create a new element and add it to the SVG element = document.createElement(elementType); if (insertBefore !== undefined) { DOMContainer.insertBefore(element, insertBefore); - } - else { + } else { DOMContainer.appendChild(element); } } - } - else { + } else { // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. element = document.createElement(elementType); - JSONcontainer[elementType] = {used: [], redundant: []}; + JSONcontainer[elementType] = { used: [], redundant: [] }; if (insertBefore !== undefined) { DOMContainer.insertBefore(element, insertBefore); - } - else { + } else { DOMContainer.appendChild(element); } } @@ -4661,44 +4610,42 @@ return /******/ (function(modules) { // webpackBootstrap * @param labelObj * @returns {*} */ - exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer, labelObj) { + exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { var point; - if (group.options.drawPoints.style == 'circle') { - point = exports.getSVGElement('circle',JSONcontainer,svgContainer); + if (group.options.drawPoints.style == "circle") { + point = exports.getSVGElement("circle", JSONcontainer, svgContainer); point.setAttributeNS(null, "cx", x); point.setAttributeNS(null, "cy", y); point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size); - } - else { - point = exports.getSVGElement('rect',JSONcontainer,svgContainer); - point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size); - point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size); + } else { + point = exports.getSVGElement("rect", JSONcontainer, svgContainer); + point.setAttributeNS(null, "x", x - 0.5 * group.options.drawPoints.size); + point.setAttributeNS(null, "y", y - 0.5 * group.options.drawPoints.size); point.setAttributeNS(null, "width", group.options.drawPoints.size); point.setAttributeNS(null, "height", group.options.drawPoints.size); } - if(group.options.drawPoints.styles !== undefined) { + if (group.options.drawPoints.styles !== undefined) { point.setAttributeNS(null, "style", group.group.options.drawPoints.styles); } point.setAttributeNS(null, "class", group.className + " point"); - //handle label - var label = exports.getSVGElement('text',JSONcontainer,svgContainer); - if (labelObj){ - if (labelObj.xOffset) { - x = x + labelObj.xOffset; - } - - if (labelObj.yOffset) { - y = y + labelObj.yOffset; - } - if (labelObj.content) { - label.textContent = labelObj.content; - } + //handle label + var label = exports.getSVGElement("text", JSONcontainer, svgContainer); + if (labelObj) { + if (labelObj.xOffset) { + x = x + labelObj.xOffset; + } - if (labelObj.className) { - label.setAttributeNS(null, "class", labelObj.className + " label"); - } + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } + if (labelObj.className) { + label.setAttributeNS(null, "class", labelObj.className + " label"); + } } label.setAttributeNS(null, "x", x); @@ -4719,7 +4666,7 @@ return /******/ (function(modules) { // webpackBootstrap height *= -1; y -= height; } - var rect = exports.getSVGElement('rect',JSONcontainer, svgContainer); + var rect = exports.getSVGElement("rect", JSONcontainer, svgContainer); rect.setAttributeNS(null, "x", x - 0.5 * width); rect.setAttributeNS(null, "y", y); rect.setAttributeNS(null, "width", width); @@ -4732,6 +4679,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 7 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var Queue = __webpack_require__(8); @@ -4778,7 +4727,7 @@ return /******/ (function(modules) { // webpackBootstrap * @constructor DataSet */ // TODO: add a DataSet constructor DataSet(data, options) - function DataSet (data, options) { + function DataSet(data, options) { // correctly read optional arguments if (data && !Array.isArray(data) && !util.isDataTable(data)) { options = data; @@ -4786,10 +4735,10 @@ return /******/ (function(modules) { // webpackBootstrap } this._options = options || {}; - this._data = {}; // map with data indexed by id - this.length = 0; // number of items in the DataSet - this._fieldId = this._options.fieldId || 'id'; // name of the field containing id - this._type = {}; // internal field types (NOTE: this can differ from this._options.type) + this._data = {}; // map with data indexed by id + this.length = 0; // number of items in the DataSet + this._fieldId = this._options.fieldId || "id"; // name of the field containing id + this._type = {}; // internal field types (NOTE: this can differ from this._options.type) // all variants of a Date are internally stored as Date, so we can convert // from everything to everything (also from ISODate to Number for example) @@ -4797,10 +4746,9 @@ return /******/ (function(modules) { // webpackBootstrap for (var field in this._options.type) { if (this._options.type.hasOwnProperty(field)) { var value = this._options.type[field]; - if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') { - this._type[field] = 'Date'; - } - else { + if (value == "Date" || value == "ISODate" || value == "ASPDate") { + this._type[field] = "Date"; + } else { this._type[field] = value; } } @@ -4809,10 +4757,10 @@ return /******/ (function(modules) { // webpackBootstrap // TODO: deprecated since version 1.1.1 (or 2.0.0?) if (this._options.convert) { - throw new Error('Option "convert" is deprecated. Use "type" instead.'); + throw new Error("Option \"convert\" is deprecated. Use \"type\" instead."); } - this._subscribers = {}; // event subscribers + this._subscribers = {}; // event subscribers // add initial data when provided if (data) { @@ -4831,7 +4779,7 @@ return /******/ (function(modules) { // webpackBootstrap * - {number} max Maximum number of entries in the queue, Infinity by default * @param options */ - DataSet.prototype.setOptions = function(options) { + DataSet.prototype.setOptions = function (options) { if (options && options.queue !== undefined) { if (options.queue === false) { // delete queue if loaded @@ -4839,16 +4787,15 @@ return /******/ (function(modules) { // webpackBootstrap this._queue.destroy(); delete this._queue; } - } - else { + } else { // create queue and update its options if (!this._queue) { this._queue = Queue.extend(this, { - replace: ['add', 'update', 'remove'] + replace: ["add", "update", "remove"] }); } - if (typeof options.queue === 'object') { + if (typeof options.queue === "object") { this._queue.setOptions(options.queue); } } @@ -4864,7 +4811,7 @@ return /******/ (function(modules) { // webpackBootstrap * {Object | null} params * {String | Number} senderId */ - DataSet.prototype.on = function(event, callback) { + DataSet.prototype.on = function (event, callback) { var subscribers = this._subscribers[event]; if (!subscribers) { subscribers = []; @@ -4884,11 +4831,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {String} event * @param {function} callback */ - DataSet.prototype.off = function(event, callback) { + DataSet.prototype.off = function (event, callback) { var subscribers = this._subscribers[event]; if (subscribers) { this._subscribers[event] = subscribers.filter(function (listener) { - return (listener.callback != callback); + return listener.callback != callback; }); } }; @@ -4904,16 +4851,16 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ DataSet.prototype._trigger = function (event, params, senderId) { - if (event == '*') { - throw new Error('Cannot trigger event *'); + if (event == "*") { + throw new Error("Cannot trigger event *"); } var subscribers = []; if (event in this._subscribers) { subscribers = subscribers.concat(this._subscribers[event]); } - if ('*' in this._subscribers) { - subscribers = subscribers.concat(this._subscribers['*']); + if ("*" in this._subscribers) { + subscribers = subscribers.concat(this._subscribers["*"]); } for (var i = 0; i < subscribers.length; i++) { @@ -4942,8 +4889,7 @@ return /******/ (function(modules) { // webpackBootstrap id = me._addItem(data[i]); addedIds.push(id); } - } - else if (util.isDataTable(data)) { + } else if (util.isDataTable(data)) { // Google DataTable var columns = this._getColumnNames(data); for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { @@ -4956,18 +4902,16 @@ return /******/ (function(modules) { // webpackBootstrap id = me._addItem(item); addedIds.push(id); } - } - else if (data instanceof Object) { + } else if (data instanceof Object) { // Single item id = me._addItem(data); addedIds.push(id); - } - else { - throw new Error('Unknown dataType'); + } else { + throw new Error("Unknown dataType"); } if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); + this._trigger("add", { items: addedIds }, senderId); } return addedIds; @@ -4993,8 +4937,7 @@ return /******/ (function(modules) { // webpackBootstrap id = me._updateItem(item); updatedIds.push(id); updatedData.push(item); - } - else { + } else { // add new item id = me._addItem(item); addedIds.push(id); @@ -5006,8 +4949,7 @@ return /******/ (function(modules) { // webpackBootstrap for (var i = 0, len = data.length; i < len; i++) { addOrUpdate(data[i]); } - } - else if (util.isDataTable(data)) { + } else if (util.isDataTable(data)) { // Google DataTable var columns = this._getColumnNames(data); for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { @@ -5019,20 +4961,18 @@ return /******/ (function(modules) { // webpackBootstrap addOrUpdate(item); } - } - else if (data instanceof Object) { + } else if (data instanceof Object) { // Single item addOrUpdate(data); - } - else { - throw new Error('Unknown dataType'); + } else { + throw new Error("Unknown dataType"); } if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); + this._trigger("add", { items: addedIds }, senderId); } if (updatedIds.length) { - this._trigger('update', {items: updatedIds, data: updatedData}, senderId); + this._trigger("update", { items: updatedIds, data: updatedData }, senderId); } return addedIds.concat(updatedIds); @@ -5079,19 +5019,17 @@ return /******/ (function(modules) { // webpackBootstrap // parse the arguments var id, ids, options, data; var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number') { + if (firstType == "String" || firstType == "Number") { // get(id [, options] [, data]) id = arguments[0]; options = arguments[1]; data = arguments[2]; - } - else if (firstType == 'Array') { + } else if (firstType == "Array") { // get(ids [, options] [, data]) ids = arguments[0]; options = arguments[1]; data = arguments[2]; - } - else { + } else { // get([, options] [, data]) options = arguments[0]; data = arguments[1]; @@ -5103,26 +5041,26 @@ return /******/ (function(modules) { // webpackBootstrap var allowedValues = ["DataTable", "Array", "Object"]; returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; - if (data && (returnType != util.getType(data))) { - throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + - 'does not correspond with specified options.type (' + options.type + ')'); + if (data && returnType != util.getType(data)) { + throw new Error("Type of parameter \"data\" (" + util.getType(data) + ") " + "does not correspond with specified options.type (" + options.type + ")"); } - if (returnType == 'DataTable' && !util.isDataTable(data)) { - throw new Error('Parameter "data" must be a DataTable ' + - 'when options.type is "DataTable"'); + if (returnType == "DataTable" && !util.isDataTable(data)) { + throw new Error("Parameter \"data\" must be a DataTable " + "when options.type is \"DataTable\""); } - } - else if (data) { - returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; - } - else { - returnType = 'Array'; + } else if (data) { + returnType = util.getType(data) == "DataTable" ? "DataTable" : "Array"; + } else { + returnType = "Array"; } // build options var type = options && options.type || this._options.type; var filter = options && options.filter; - var items = [], item, itemId, i, len; + var items = [], + item, + itemId, + i, + len; // convert items if (id != undefined) { @@ -5131,8 +5069,7 @@ return /******/ (function(modules) { // webpackBootstrap if (filter && !filter(item)) { item = null; } - } - else if (ids != undefined) { + } else if (ids != undefined) { // return a subset of items for (i = 0, len = ids.length; i < len; i++) { item = me._getItem(ids[i], type); @@ -5140,8 +5077,7 @@ return /******/ (function(modules) { // webpackBootstrap items.push(item); } } - } - else { + } else { // return all items for (itemId in this._data) { if (this._data.hasOwnProperty(itemId)) { @@ -5163,8 +5099,7 @@ return /******/ (function(modules) { // webpackBootstrap var fields = options.fields; if (id != undefined) { item = this._filterFields(item, fields); - } - else { + } else { for (i = 0, len = items.length; i < len; i++) { items[i] = this._filterFields(items[i], fields); } @@ -5172,34 +5107,30 @@ return /******/ (function(modules) { // webpackBootstrap } // return the results - if (returnType == 'DataTable') { + if (returnType == "DataTable") { var columns = this._getColumnNames(data); if (id != undefined) { // append a single item to the data table me._appendRow(data, columns, item); - } - else { + } else { // copy the items to the provided data table for (i = 0; i < items.length; i++) { me._appendRow(data, columns, items[i]); } } return data; - } - else if (returnType == "Object") { + } else if (returnType == "Object") { var result = {}; for (i = 0; i < items.length; i++) { result[items[i].id] = items[i]; } return result; - } - else { + } else { // return an array if (id != undefined) { // a single item return item; - } - else { + } else { // multiple items if (data) { // copy the items to the provided array @@ -5207,8 +5138,7 @@ return /******/ (function(modules) { // webpackBootstrap data.push(items[i]); } return data; - } - else { + } else { // just return our array return items; } @@ -5255,8 +5185,7 @@ return /******/ (function(modules) { // webpackBootstrap for (i = 0, len = items.length; i < len; i++) { ids[i] = items[i][this._fieldId]; } - } - else { + } else { // create unordered list for (id in data) { if (data.hasOwnProperty(id)) { @@ -5267,8 +5196,7 @@ return /******/ (function(modules) { // webpackBootstrap } } } - } - else { + } else { // get all items if (order) { // create an ordered list @@ -5284,8 +5212,7 @@ return /******/ (function(modules) { // webpackBootstrap for (i = 0, len = items.length; i < len; i++) { ids[i] = items[i][this._fieldId]; } - } - else { + } else { // create unordered list for (id in data) { if (data.hasOwnProperty(id)) { @@ -5333,8 +5260,7 @@ return /******/ (function(modules) { // webpackBootstrap id = item[this._fieldId]; callback(item, id); } - } - else { + } else { // unordered for (id in data) { if (data.hasOwnProperty(id)) { @@ -5391,14 +5317,15 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ DataSet.prototype._filterFields = function (item, fields) { - if (!item) { // item is null + if (!item) { + // item is null return item; } var filteredItem = {}; for (var field in item) { - if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { filteredItem[field] = item[field]; } } @@ -5419,17 +5346,16 @@ return /******/ (function(modules) { // webpackBootstrap items.sort(function (a, b) { var av = a[name]; var bv = b[name]; - return (av > bv) ? 1 : ((av < bv) ? -1 : 0); + return av > bv ? 1 : av < bv ? -1 : 0; }); - } - else if (typeof order === 'function') { + } else if (typeof order === "function") { // order by sort function items.sort(order); } // TODO: extend order by an Object {field:String, direction:String} // where direction can be 'asc' or 'desc' else { - throw new TypeError('Order must be a function or a string'); + throw new TypeError("Order must be a function or a string"); } }; @@ -5442,7 +5368,9 @@ return /******/ (function(modules) { // webpackBootstrap */ DataSet.prototype.remove = function (id, senderId) { var removedIds = [], - i, len, removedId; + i, + len, + removedId; if (Array.isArray(id)) { for (i = 0, len = id.length; i < len; i++) { @@ -5451,8 +5379,7 @@ return /******/ (function(modules) { // webpackBootstrap removedIds.push(removedId); } } - } - else { + } else { removedId = this._remove(id); if (removedId != null) { removedIds.push(removedId); @@ -5460,7 +5387,7 @@ return /******/ (function(modules) { // webpackBootstrap } if (removedIds.length) { - this._trigger('remove', {items: removedIds}, senderId); + this._trigger("remove", { items: removedIds }, senderId); } return removedIds; @@ -5479,8 +5406,7 @@ return /******/ (function(modules) { // webpackBootstrap this.length--; return id; } - } - else if (id instanceof Object) { + } else if (id instanceof Object) { var itemId = id[this._fieldId]; if (itemId && this._data[itemId]) { delete this._data[itemId]; @@ -5502,7 +5428,7 @@ return /******/ (function(modules) { // webpackBootstrap this._data = {}; this.length = 0; - this._trigger('remove', {items: ids}, senderId); + this._trigger("remove", { items: ids }, senderId); return ids; }; @@ -5580,7 +5506,7 @@ return /******/ (function(modules) { // webpackBootstrap break; } } - if (!exists && (value !== undefined)) { + if (!exists && value !== undefined) { values[count] = value; count++; } @@ -5609,10 +5535,9 @@ return /******/ (function(modules) { // webpackBootstrap // check whether this id is already taken if (this._data[id]) { // item already exists - throw new Error('Cannot add item: item with id ' + id + ' already exists'); + throw new Error("Cannot add item: item with id " + id + " already exists"); } - } - else { + } else { // generate an id id = util.randomUUID(); item[this._fieldId] = id; @@ -5621,7 +5546,7 @@ return /******/ (function(modules) { // webpackBootstrap var d = {}; for (var field in item) { if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined + var fieldType = this._type[field]; // type may be undefined d[field] = util.convert(item[field], fieldType); } } @@ -5656,8 +5581,7 @@ return /******/ (function(modules) { // webpackBootstrap converted[field] = util.convert(value, types[field]); } } - } - else { + } else { // no field types specified, no converting needed for (field in raw) { if (raw.hasOwnProperty(field)) { @@ -5680,18 +5604,18 @@ return /******/ (function(modules) { // webpackBootstrap DataSet.prototype._updateItem = function (item) { var id = item[this._fieldId]; if (id == undefined) { - throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + throw new Error("Cannot update item: item has no id (item: " + JSON.stringify(item) + ")"); } var d = this._data[id]; if (!d) { // item doesn't exist - throw new Error('Cannot update item: no item with id ' + id + ' found'); + throw new Error("Cannot update item: no item with id " + id + " found"); } // merge with current item for (var field in item) { if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined + var fieldType = this._type[field]; // type may be undefined d[field] = util.convert(item[field], fieldType); } } @@ -5731,11 +5655,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = DataSet; - /***/ }, /* 8 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * A queue * @param {Object} options @@ -5776,10 +5701,10 @@ return /******/ (function(modules) { // webpackBootstrap * @param options */ Queue.prototype.setOptions = function (options) { - if (options && typeof options.delay !== 'undefined') { + if (options && typeof options.delay !== "undefined") { this.delay = options.delay; } - if (options && typeof options.max !== 'undefined') { + if (options && typeof options.max !== "undefined") { this.max = options.max; } @@ -5809,14 +5734,14 @@ return /******/ (function(modules) { // webpackBootstrap var queue = new Queue(options); if (object.flush !== undefined) { - throw new Error('Target object already has a property flush'); + throw new Error("Target object already has a property flush"); } object.flush = function () { queue.flush(); }; var methods = [{ - name: 'flush', + name: "flush", original: undefined }]; @@ -5853,8 +5778,7 @@ return /******/ (function(modules) { // webpackBootstrap var method = methods[i]; if (method.original) { object[method.name] = method.original; - } - else { + } else { delete object[method.name]; } } @@ -5867,11 +5791,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} object Object having the method * @param {string} method The method name */ - Queue.prototype.replace = function(object, method) { + Queue.prototype.replace = function (object, method) { var me = this; var original = object[method]; if (!original) { - throw new Error('Method ' + method + ' undefined'); + throw new Error("Method " + method + " undefined"); } object[method] = function () { @@ -5894,11 +5818,10 @@ return /******/ (function(modules) { // webpackBootstrap * Queue a call * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry */ - Queue.prototype.queue = function(entry) { - if (typeof entry === 'function') { - this._queue.push({fn: entry}); - } - else { + Queue.prototype.queue = function (entry) { + if (typeof entry === "function") { + this._queue.push({ fn: entry }); + } else { this._queue.push(entry); } @@ -5917,7 +5840,7 @@ return /******/ (function(modules) { // webpackBootstrap // flush after a period of inactivity when a delay is configured clearTimeout(this._timeout); - if (this.queue.length > 0 && typeof this.delay === 'number') { + if (this.queue.length > 0 && typeof this.delay === "number") { var me = this; this._timeout = setTimeout(function () { me.flush(); @@ -5937,11 +5860,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Queue; - /***/ }, /* 9 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var DataSet = __webpack_require__(7); @@ -5955,12 +5879,12 @@ return /******/ (function(modules) { // webpackBootstrap * * @constructor DataView */ - function DataView (data, options) { + function DataView(data, options) { this._data = null; this._ids = {}; // ids of the items currently in memory (just contains a boolean true) this.length = 0; // number of items in the DataView this._options = options || {}; - this._fieldId = 'id'; // name of the field containing id + this._fieldId = "id"; // name of the field containing id this._subscribers = {}; // event subscribers var me = this; @@ -5984,7 +5908,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this._data) { // unsubscribe from current dataset if (this._data.unsubscribe) { - this._data.unsubscribe('*', this.listener); + this._data.unsubscribe("*", this.listener); } // trigger a remove of all items in memory @@ -5996,29 +5920,27 @@ return /******/ (function(modules) { // webpackBootstrap } this._ids = {}; this.length = 0; - this._trigger('remove', {items: ids}); + this._trigger("remove", { items: ids }); } this._data = data; if (this._data) { // update fieldId - this._fieldId = this._options.fieldId || - (this._data && this._data.options && this._data.options.fieldId) || - 'id'; + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; // trigger an add of all added items - ids = this._data.getIds({filter: this._options && this._options.filter}); + ids = this._data.getIds({ filter: this._options && this._options.filter }); for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; this._ids[id] = true; } this.length = ids.length; - this._trigger('add', {items: ids}); + this._trigger("add", { items: ids }); // subscribe to new dataset if (this._data.on) { - this._data.on('*', this.listener); + this._data.on("*", this.listener); } } }; @@ -6029,7 +5951,7 @@ return /******/ (function(modules) { // webpackBootstrap */ DataView.prototype.refresh = function () { var id; - var ids = this._data.getIds({filter: this._options && this._options.filter}); + var ids = this._data.getIds({ filter: this._options && this._options.filter }); var newIds = {}; var added = []; var removed = []; @@ -6058,10 +5980,10 @@ return /******/ (function(modules) { // webpackBootstrap // trigger events if (added.length) { - this._trigger('add', {items: added}); + this._trigger("add", { items: added }); } if (removed.length) { - this._trigger('remove', {items: removed}); + this._trigger("remove", { items: removed }); } }; @@ -6105,13 +6027,12 @@ return /******/ (function(modules) { // webpackBootstrap // parse the arguments var ids, options, data; var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { + if (firstType == "String" || firstType == "Number" || firstType == "Array") { // get(id(s) [, options] [, data]) - ids = arguments[0]; // can be a single id or an array with ids + ids = arguments[0]; // can be a single id or an array with ids options = arguments[1]; data = arguments[2]; - } - else { + } else { // get([, options] [, data]) options = arguments[0]; data = arguments[1]; @@ -6124,7 +6045,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this._options.filter && options && options.filter) { viewOptions.filter = function (item) { return me._options.filter(item) && options.filter(item); - } + }; } // build up the call to the linked data set @@ -6157,13 +6078,11 @@ return /******/ (function(modules) { // webpackBootstrap if (defaultFilter) { filter = function (item) { return defaultFilter(item) && options.filter(item); - } - } - else { + }; + } else { filter = options.filter; } - } - else { + } else { filter = defaultFilter; } @@ -6171,8 +6090,7 @@ return /******/ (function(modules) { // webpackBootstrap filter: filter, order: options && options.order }); - } - else { + } else { ids = []; } @@ -6202,16 +6120,19 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item, - ids = params && params.items, - data = this._data, + var i, + len, + id, + item, + ids = params && params.items, + data = this._data, added = [], updated = [], removed = []; if (ids && data) { switch (event) { - case 'add': + case "add": // filter the ids of the added items for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; @@ -6224,7 +6145,7 @@ return /******/ (function(modules) { // webpackBootstrap break; - case 'update': + case "update": // determine the event from the views viewpoint: an updated // item can be added, updated, or removed from this view. for (i = 0, len = ids.length; i < len; i++) { @@ -6234,26 +6155,21 @@ return /******/ (function(modules) { // webpackBootstrap if (item) { if (this._ids[id]) { updated.push(id); - } - else { + } else { this._ids[id] = true; added.push(id); } - } - else { + } else { if (this._ids[id]) { delete this._ids[id]; removed.push(id); - } - else { - // nothing interesting for me :-( - } + } else {} } } break; - case 'remove': + case "remove": // filter the ids of the removed items for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; @@ -6269,13 +6185,13 @@ return /******/ (function(modules) { // webpackBootstrap this.length += added.length - removed.length; if (added.length) { - this._trigger('add', {items: added}, senderId); + this._trigger("add", { items: added }, senderId); } if (updated.length) { - this._trigger('update', {items: updated}, senderId); + this._trigger("update", { items: updated }, senderId); } if (removed.length) { - this._trigger('remove', {items: removed}, senderId); + this._trigger("remove", { items: removed }, senderId); } } }; @@ -6290,11 +6206,14 @@ return /******/ (function(modules) { // webpackBootstrap DataView.prototype.unsubscribe = DataView.prototype.off; module.exports = DataView; + // nothing interesting for me :-( /***/ }, /* 10 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Emitter = __webpack_require__(11); var DataSet = __webpack_require__(7); var DataView = __webpack_require__(9); @@ -6319,28 +6238,30 @@ return /******/ (function(modules) { // webpackBootstrap */ function Graph3d(container, data, options) { if (!(this instanceof Graph3d)) { - throw new SyntaxError('Constructor must be called with the new operator'); + throw new SyntaxError("Constructor must be called with the new operator"); } // create variables and set default values this.containerElement = container; - this.width = '400px'; - this.height = '400px'; + this.width = "400px"; + this.height = "400px"; this.margin = 10; // px - this.defaultXCenter = '55%'; - this.defaultYCenter = '50%'; + this.defaultXCenter = "55%"; + this.defaultYCenter = "50%"; - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; + this.xLabel = "x"; + this.yLabel = "y"; + this.zLabel = "z"; - var passValueFn = function(v) { return v; }; + var passValueFn = function (v) { + return v; + }; this.xValueLabel = passValueFn; this.yValueLabel = passValueFn; this.zValueLabel = passValueFn; - - this.filterLabel = 'time'; - this.legendLabel = 'value'; + + this.filterLabel = "time"; + this.legendLabel = "value"; this.style = Graph3d.STYLE.DOT; this.showPerspective = true; @@ -6355,9 +6276,9 @@ return /******/ (function(modules) { // webpackBootstrap this.animationPreload = false; this.camera = new Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - this.dataTable = null; // The original data table + this.dataTable = null; // The original data table this.dataPoints = null; // The table with point objects // the column indexes @@ -6383,10 +6304,10 @@ return /******/ (function(modules) { // webpackBootstrap // TODO: customize axis range // constants - this.colorAxis = '#4D4D4D'; - this.colorGrid = '#D3D3D3'; - this.colorDot = '#7DC1FF'; - this.colorDotBorder = '#3267D2'; + this.colorAxis = "#4D4D4D"; + this.colorGrid = "#D3D3D3"; + this.colorDot = "#7DC1FF"; + this.colorDotBorder = "#3267D2"; // create a frame and canvas this.create(); @@ -6406,18 +6327,15 @@ return /******/ (function(modules) { // webpackBootstrap /** * Calculate the scaling values, dependent on the range in x, y, and z direction */ - Graph3d.prototype._setScale = function() { - this.scale = new Point3d(1 / (this.xMax - this.xMin), - 1 / (this.yMax - this.yMin), - 1 / (this.zMax - this.zMin)); + Graph3d.prototype._setScale = function () { + this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); // keep aspect ration between x and y scale if desired if (this.keepAspectRatio) { if (this.scale.x < this.scale.y) { //noinspection JSSuspiciousNameCombination this.scale.y = this.scale.x; - } - else { + } else { //noinspection JSSuspiciousNameCombination this.scale.x = this.scale.y; } @@ -6444,7 +6362,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Point3d} point3d A 3D point with parameters x, y, z * @return {Point2d} point2d A 2D point with parameters x, y */ - Graph3d.prototype._convert3Dto2D = function(point3d) { + Graph3d.prototype._convert3Dto2D = function (point3d) { var translation = this._convertPointToTranslation(point3d); return this._convertTranslationToScreen(translation); }; @@ -6457,27 +6375,28 @@ return /******/ (function(modules) { // webpackBootstrap * the translation of the point, seen from the * camera */ - Graph3d.prototype._convertPointToTranslation = function(point3d) { + Graph3d.prototype._convertPointToTranslation = function (point3d) { var ax = point3d.x * this.scale.x, - ay = point3d.y * this.scale.y, - az = point3d.z * this.scale.z, + ay = point3d.y * this.scale.y, + az = point3d.z * this.scale.z, + cx = this.camera.getCameraLocation().x, + cy = this.camera.getCameraLocation().y, + cz = this.camera.getCameraLocation().z, - cx = this.camera.getCameraLocation().x, - cy = this.camera.getCameraLocation().y, - cz = this.camera.getCameraLocation().z, // calculate angles - sinTx = Math.sin(this.camera.getCameraRotation().x), - cosTx = Math.cos(this.camera.getCameraRotation().x), - sinTy = Math.sin(this.camera.getCameraRotation().y), - cosTy = Math.cos(this.camera.getCameraRotation().y), - sinTz = Math.sin(this.camera.getCameraRotation().z), - cosTz = Math.cos(this.camera.getCameraRotation().z), + sinTx = Math.sin(this.camera.getCameraRotation().x), + cosTx = Math.cos(this.camera.getCameraRotation().x), + sinTy = Math.sin(this.camera.getCameraRotation().y), + cosTy = Math.cos(this.camera.getCameraRotation().y), + sinTz = Math.sin(this.camera.getCameraRotation().z), + cosTz = Math.cos(this.camera.getCameraRotation().z), + // calculate translation - dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), - dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)), - dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx)); + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); return new Point3d(dx, dy, dz); }; @@ -6489,13 +6408,13 @@ return /******/ (function(modules) { // webpackBootstrap * camera * @return {Point2d} point2d A 2D point with parameters x, y */ - Graph3d.prototype._convertTranslationToScreen = function(translation) { + Graph3d.prototype._convertTranslationToScreen = function (translation) { var ex = this.eye.x, - ey = this.eye.y, - ez = this.eye.z, - dx = translation.x, - dy = translation.y, - dz = translation.z; + ey = this.eye.y, + ez = this.eye.z, + dx = translation.x, + dy = translation.y, + dz = translation.z; // calculate position on screen from translation var bx; @@ -6503,49 +6422,41 @@ return /******/ (function(modules) { // webpackBootstrap if (this.showPerspective) { bx = (dx - ex) * (ez / dz); by = (dy - ey) * (ez / dz); - } - else { + } else { bx = dx * -(ez / this.camera.getArmLength()); by = dy * -(ez / this.camera.getArmLength()); } // shift and scale the point to the center of the screen // use the width of the graph to scale both horizontally and vertically. - return new Point2d( - this.xcenter + bx * this.frame.canvas.clientWidth, - this.ycenter - by * this.frame.canvas.clientWidth); + return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); }; /** * Set the background styling for the graph * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ - Graph3d.prototype._setBackgroundColor = function(backgroundColor) { - var fill = 'white'; - var stroke = 'gray'; + Graph3d.prototype._setBackgroundColor = function (backgroundColor) { + var fill = "white"; + var stroke = "gray"; var strokeWidth = 1; - if (typeof(backgroundColor) === 'string') { + if (typeof backgroundColor === "string") { fill = backgroundColor; - stroke = 'none'; + stroke = "none"; strokeWidth = 0; - } - else if (typeof(backgroundColor) === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + } else if (typeof backgroundColor === "object") { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } - else if (backgroundColor === undefined) { - // use use defaults - } - else { - throw 'Unsupported type of backgroundColor'; + } else if (backgroundColor === undefined) {} else { + throw "Unsupported type of backgroundColor"; } this.frame.style.backgroundColor = fill; this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; }; @@ -6554,13 +6465,13 @@ return /******/ (function(modules) { // webpackBootstrap BAR: 0, BARCOLOR: 1, BARSIZE: 2, - DOT : 3, - DOTLINE : 4, + DOT: 3, + DOTLINE: 4, DOTCOLOR: 5, DOTSIZE: 6, - GRID : 7, + GRID: 7, LINE: 8, - SURFACE : 9 + SURFACE: 9 }; /** @@ -6569,18 +6480,28 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Number} styleNumber Enumeration value representing the style, or -1 * when not found */ - Graph3d.prototype._getStyleNumber = function(styleName) { + Graph3d.prototype._getStyleNumber = function (styleName) { switch (styleName) { - case 'dot': return Graph3d.STYLE.DOT; - case 'dot-line': return Graph3d.STYLE.DOTLINE; - case 'dot-color': return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': return Graph3d.STYLE.DOTSIZE; - case 'line': return Graph3d.STYLE.LINE; - case 'grid': return Graph3d.STYLE.GRID; - case 'surface': return Graph3d.STYLE.SURFACE; - case 'bar': return Graph3d.STYLE.BAR; - case 'bar-color': return Graph3d.STYLE.BARCOLOR; - case 'bar-size': return Graph3d.STYLE.BARSIZE; + case "dot": + return Graph3d.STYLE.DOT; + case "dot-line": + return Graph3d.STYLE.DOTLINE; + case "dot-color": + return Graph3d.STYLE.DOTCOLOR; + case "dot-size": + return Graph3d.STYLE.DOTSIZE; + case "line": + return Graph3d.STYLE.LINE; + case "grid": + return Graph3d.STYLE.GRID; + case "surface": + return Graph3d.STYLE.SURFACE; + case "bar": + return Graph3d.STYLE.BAR; + case "bar-color": + return Graph3d.STYLE.BARCOLOR; + case "bar-size": + return Graph3d.STYLE.BARSIZE; } return -1; @@ -6591,13 +6512,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {DataSet} data * @param {Number} style */ - Graph3d.prototype._determineColumnIndexes = function(data, style) { - if (this.style === Graph3d.STYLE.DOT || - this.style === Graph3d.STYLE.DOTLINE || - this.style === Graph3d.STYLE.LINE || - this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE || - this.style === Graph3d.STYLE.BAR) { + Graph3d.prototype._determineColumnIndexes = function (data, style) { + if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { // 3 columns expected, and optionally a 4th with filter values this.colX = 0; this.colY = 1; @@ -6607,11 +6523,7 @@ return /******/ (function(modules) { // webpackBootstrap if (data.getNumberOfColumns() > 3) { this.colFilter = 3; } - } - else if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { + } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { // 4 columns expected, and optionally a 5th with filter values this.colX = 0; this.colY = 1; @@ -6621,18 +6533,17 @@ return /******/ (function(modules) { // webpackBootstrap if (data.getNumberOfColumns() > 4) { this.colFilter = 4; } - } - else { - throw 'Unknown style "' + this.style + '"'; + } else { + throw "Unknown style \"" + this.style + "\""; } }; - Graph3d.prototype.getNumberOfRows = function(data) { + Graph3d.prototype.getNumberOfRows = function (data) { return data.length; - } + }; - Graph3d.prototype.getNumberOfColumns = function(data) { + Graph3d.prototype.getNumberOfColumns = function (data) { var counter = 0; for (var column in data[0]) { if (data[0].hasOwnProperty(column)) { @@ -6640,10 +6551,10 @@ return /******/ (function(modules) { // webpackBootstrap } } return counter; - } + }; - Graph3d.prototype.getDistinctValues = function(data, column) { + Graph3d.prototype.getDistinctValues = function (data, column) { var distinctValues = []; for (var i = 0; i < data.length; i++) { if (distinctValues.indexOf(data[i][column]) == -1) { @@ -6651,14 +6562,18 @@ return /******/ (function(modules) { // webpackBootstrap } } return distinctValues; - } + }; - Graph3d.prototype.getColumnRange = function(data,column) { - var minMax = {min:data[0][column],max:data[0][column]}; + Graph3d.prototype.getColumnRange = function (data, column) { + var minMax = { min: data[0][column], max: data[0][column] }; for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { minMax.min = data[i][column]; } - if (minMax.max < data[i][column]) { minMax.max = data[i][column]; } + if (minMax.min > data[i][column]) { + minMax.min = data[i][column]; + } + if (minMax.max < data[i][column]) { + minMax.max = data[i][column]; + } } return minMax; }; @@ -6674,11 +6589,10 @@ return /******/ (function(modules) { // webpackBootstrap // unsubscribe from the dataTable if (this.dataSet) { - this.dataSet.off('*', this._onChange); + this.dataSet.off("*", this._onChange); } - if (rawData === undefined) - return; + if (rawData === undefined) return; if (Array.isArray(rawData)) { rawData = new DataSet(rawData); @@ -6687,13 +6601,11 @@ return /******/ (function(modules) { // webpackBootstrap var data; if (rawData instanceof DataSet || rawData instanceof DataView) { data = rawData.get(); - } - else { - throw new Error('Array, DataSet, or DataView expected'); + } else { + throw new Error("Array, DataSet, or DataView expected"); } - if (data.length == 0) - return; + if (data.length == 0) return; this.dataSet = rawData; this.dataTable = data; @@ -6702,7 +6614,7 @@ return /******/ (function(modules) { // webpackBootstrap this._onChange = function () { me.setData(me.dataSet); }; - this.dataSet.on('*', this._onChange); + this.dataSet.on("*", this._onChange); // _determineColumnIndexes // getNumberOfRows (points) @@ -6711,77 +6623,75 @@ return /******/ (function(modules) { // webpackBootstrap // getColumnRange // determine the location of x,y,z,value,filter columns - this.colX = 'x'; - this.colY = 'y'; - this.colZ = 'z'; - this.colValue = 'style'; - this.colFilter = 'filter'; + this.colX = "x"; + this.colY = "y"; + this.colZ = "z"; + this.colValue = "style"; + this.colFilter = "filter"; // check if a filter column is provided - if (data[0].hasOwnProperty('filter')) { + if (data[0].hasOwnProperty("filter")) { if (this.dataFilter === undefined) { this.dataFilter = new Filter(rawData, this.colFilter, this); - this.dataFilter.setOnLoadCallback(function() {me.redraw();}); + this.dataFilter.setOnLoadCallback(function () { + me.redraw(); + }); } } - var withBars = this.style == Graph3d.STYLE.BAR || - this.style == Graph3d.STYLE.BARCOLOR || - this.style == Graph3d.STYLE.BARSIZE; + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; // determine barWidth from data if (withBars) { if (this.defaultXBarWidth !== undefined) { this.xBarWidth = this.defaultXBarWidth; - } - else { - var dataX = this.getDistinctValues(data,this.colX); - this.xBarWidth = (dataX[1] - dataX[0]) || 1; + } else { + var dataX = this.getDistinctValues(data, this.colX); + this.xBarWidth = dataX[1] - dataX[0] || 1; } if (this.defaultYBarWidth !== undefined) { this.yBarWidth = this.defaultYBarWidth; - } - else { - var dataY = this.getDistinctValues(data,this.colY); - this.yBarWidth = (dataY[1] - dataY[0]) || 1; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; } } // calculate minimums and maximums - var xRange = this.getColumnRange(data,this.colX); + var xRange = this.getColumnRange(data, this.colX); if (withBars) { xRange.min -= this.xBarWidth / 2; xRange.max += this.xBarWidth / 2; } - this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min; - this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max; + this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; + this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; - this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5; + this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; - var yRange = this.getColumnRange(data,this.colY); + var yRange = this.getColumnRange(data, this.colY); if (withBars) { yRange.min -= this.yBarWidth / 2; yRange.max += this.yBarWidth / 2; } - this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min; - this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max; + this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; + this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; - this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5; + this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; - var zRange = this.getColumnRange(data,this.colZ); - this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min; - this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max; + var zRange = this.getColumnRange(data, this.colZ); + this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; + this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; - this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; + this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; if (this.colValue !== undefined) { - var valueRange = this.getColumnRange(data,this.colValue); - this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; - this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; + var valueRange = this.getColumnRange(data, this.colValue); + this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; + this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; } @@ -6802,8 +6712,7 @@ return /******/ (function(modules) { // webpackBootstrap var dataPoints = []; - if (this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE) { + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { // copy all values from the google data table to a matrix // the provided values are supposed to form a grid of (x,y) positions @@ -6829,13 +6738,13 @@ return /******/ (function(modules) { // webpackBootstrap dataY.sort(sortNumber); // create a grid, a 2d matrix, with all values. - var dataMatrix = []; // temporary data matrix + var dataMatrix = []; // temporary data matrix for (i = 0; i < data.length; i++) { x = data[i][this.colX] || 0; y = data[i][this.colY] || 0; z = data[i][this.colZ] || 0; - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer var yIndex = dataY.indexOf(y); if (dataMatrix[xIndex] === undefined) { @@ -6862,17 +6771,14 @@ return /******/ (function(modules) { // webpackBootstrap for (x = 0; x < dataMatrix.length; x++) { for (y = 0; y < dataMatrix[x].length; y++) { if (dataMatrix[x][y]) { - dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined; - dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined; - dataMatrix[x][y].pointCross = - (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ? - dataMatrix[x+1][y+1] : - undefined; + dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined; + dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined; + dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined; } } } - } - else { // 'dot', 'dot-line', etc. + } else { + // 'dot', 'dot-line', etc. // copy all values from the google data table to a list with Point3d objects for (i = 0; i < data.length; i++) { point = new Point3d(); @@ -6909,44 +6815,52 @@ return /******/ (function(modules) { // webpackBootstrap this.containerElement.removeChild(this.containerElement.firstChild); } - this.frame = document.createElement('div'); - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + this.frame = document.createElement("div"); + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; // create the graph canvas (HTML canvas element) - this.frame.canvas = document.createElement( 'canvas' ); - this.frame.canvas.style.position = 'relative'; + this.frame.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'; + var noCanvas = document.createElement("DIV"); + noCanvas.style.color = "red"; + noCanvas.style.fontWeight = "bold"; + noCanvas.style.padding = "10px"; + noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; this.frame.canvas.appendChild(noCanvas); } - this.frame.filter = document.createElement( 'div' ); - this.frame.filter.style.position = 'absolute'; - this.frame.filter.style.bottom = '0px'; - this.frame.filter.style.left = '0px'; - this.frame.filter.style.width = '100%'; + this.frame.filter = document.createElement("div"); + this.frame.filter.style.position = "absolute"; + this.frame.filter.style.bottom = "0px"; + this.frame.filter.style.left = "0px"; + this.frame.filter.style.width = "100%"; this.frame.appendChild(this.frame.filter); // add event listeners to handle moving and zooming the contents var me = this; - var onmousedown = function (event) {me._onMouseDown(event);}; - var ontouchstart = function (event) {me._onTouchStart(event);}; - var onmousewheel = function (event) {me._onWheel(event);}; - var ontooltip = function (event) {me._onTooltip(event);}; + var onmousedown = function (event) { + me._onMouseDown(event); + }; + var ontouchstart = function (event) { + me._onTouchStart(event); + }; + var onmousewheel = function (event) { + me._onWheel(event); + }; + var ontooltip = function (event) { + me._onTooltip(event); + }; // TODO: these events are never cleaned up... can give a 'memory leakage' - util.addEventListener(this.frame.canvas, 'keydown', onkeydown); - util.addEventListener(this.frame.canvas, 'mousedown', onmousedown); - util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart); - util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel); - util.addEventListener(this.frame.canvas, 'mousemove', ontooltip); + util.addEventListener(this.frame.canvas, "keydown", onkeydown); + util.addEventListener(this.frame.canvas, "mousedown", onmousedown); + util.addEventListener(this.frame.canvas, "touchstart", ontouchstart); + util.addEventListener(this.frame.canvas, "mousewheel", onmousewheel); + util.addEventListener(this.frame.canvas, "mousemove", ontooltip); // add the new graph to the container element this.containerElement.appendChild(this.frame); @@ -6960,7 +6874,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {string} height Height in pixels or percentage (for example '400px' * or '30%') */ - Graph3d.prototype.setSize = function(width, height) { + Graph3d.prototype.setSize = function (width, height) { this.frame.style.width = width; this.frame.style.height = height; @@ -6970,23 +6884,22 @@ return /******/ (function(modules) { // webpackBootstrap /** * Resize the canvas to the current size of the frame */ - Graph3d.prototype._resizeCanvas = function() { - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + Graph3d.prototype._resizeCanvas = function () { + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.height = this.frame.canvas.clientHeight; // adjust with for margin - this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px'; + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + "px"; }; /** * Start animation */ - Graph3d.prototype.animationStart = function() { - if (!this.frame.filter || !this.frame.filter.slider) - throw 'No animation available'; + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; this.frame.filter.slider.play(); }; @@ -6995,7 +6908,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Stop animation */ - Graph3d.prototype.animationStop = function() { + Graph3d.prototype.animationStop = function () { if (!this.frame.filter || !this.frame.filter.slider) return; this.frame.filter.slider.stop(); @@ -7008,24 +6921,18 @@ return /******/ (function(modules) { // webpackBootstrap * in pixels). The center positions are the variables this.xCenter * and this.yCenter */ - Graph3d.prototype._resizeCenter = function() { + Graph3d.prototype._resizeCenter = function () { // calculate the horizontal center position - if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') { - this.xcenter = - parseFloat(this.defaultXCenter) / 100 * - this.frame.canvas.clientWidth; - } - else { + if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === "%") { + this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; + } else { this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px } // calculate the vertical center position - if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') { - this.ycenter = - parseFloat(this.defaultYCenter) / 100 * - (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); - } - else { + if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === "%") { + this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); + } else { this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px } }; @@ -7046,7 +6953,7 @@ return /******/ (function(modules) { // webpackBootstrap * center of the graph, a value between 0.71 and 5.0. * Optional, can be left undefined. */ - Graph3d.prototype.setCameraPosition = function(pos) { + Graph3d.prototype.setCameraPosition = function (pos) { if (pos === undefined) { return; } @@ -7068,7 +6975,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {object} An object with parameters horizontal, vertical, and * distance */ - Graph3d.prototype.getCameraPosition = function() { + Graph3d.prototype.getCameraPosition = function () { var pos = this.camera.getArmRotation(); pos.distance = this.camera.getArmLength(); return pos; @@ -7077,7 +6984,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Load data into the 3D Graph */ - Graph3d.prototype._readData = function(data) { + Graph3d.prototype._readData = function (data) { // read the data this._dataInitialize(data, this.style); @@ -7085,8 +6992,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.dataFilter) { // apply filtering this.dataPoints = this.dataFilter._getDataPoints(); - } - else { + } else { // no filtering. load all data this.dataPoints = this._getDataPoints(this.dataTable); } @@ -7120,21 +7026,21 @@ return /******/ (function(modules) { // webpackBootstrap if (options !== undefined) { // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; - if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; - if (options.xLabel !== undefined) this.xLabel = options.xLabel; - if (options.yLabel !== undefined) this.yLabel = options.yLabel; - if (options.zLabel !== undefined) this.zLabel = options.zLabel; + if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; + if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; + if (options.xLabel !== undefined) this.xLabel = options.xLabel; + if (options.yLabel !== undefined) this.yLabel = options.yLabel; + if (options.zLabel !== undefined) this.zLabel = options.zLabel; - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; + if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; + if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; if (options.style !== undefined) { var styleNumber = this._getStyleNumber(options.style); @@ -7142,17 +7048,17 @@ return /******/ (function(modules) { // webpackBootstrap this.style = styleNumber; } } - if (options.showGrid !== undefined) this.showGrid = options.showGrid; - if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; - if (options.showShadow !== undefined) this.showShadow = options.showShadow; - if (options.tooltip !== undefined) this.showTooltip = options.tooltip; + if (options.showGrid !== undefined) this.showGrid = options.showGrid; + if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; + if (options.showShadow !== undefined) this.showShadow = options.showShadow; + if (options.tooltip !== undefined) this.showTooltip = options.tooltip; if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; - if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; - if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; + if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; + if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; @@ -7174,9 +7080,8 @@ return /******/ (function(modules) { // webpackBootstrap if (cameraPosition !== undefined) { this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); this.camera.setArmLength(cameraPosition.distance); - } - else { - this.camera.setArmRotation(1.0, 0.5); + } else { + this.camera.setArmRotation(1, 0.5); this.camera.setArmLength(1.7); } } @@ -7199,9 +7104,9 @@ return /******/ (function(modules) { // webpackBootstrap /** * Redraw the Graph. */ - Graph3d.prototype.redraw = function() { + Graph3d.prototype.redraw = function () { if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; + throw "Error: graph data not initialized"; } this._resizeCanvas(); @@ -7210,19 +7115,13 @@ return /******/ (function(modules) { // webpackBootstrap this._redrawClear(); this._redrawAxis(); - if (this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE) { + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { this._redrawDataGrid(); - } - else if (this.style === Graph3d.STYLE.LINE) { + } else if (this.style === Graph3d.STYLE.LINE) { this._redrawDataLine(); - } - else if (this.style === Graph3d.STYLE.BAR || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { + } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { this._redrawDataBar(); - } - else { + } else { // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE this._redrawDataDot(); } @@ -7234,9 +7133,9 @@ return /******/ (function(modules) { // webpackBootstrap /** * Clear the canvas before redrawing */ - Graph3d.prototype._redrawClear = function() { + Graph3d.prototype._redrawClear = function () { var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); }; @@ -7245,20 +7144,17 @@ return /******/ (function(modules) { // webpackBootstrap /** * Redraw the legend showing the colors */ - Graph3d.prototype._redrawLegend = function() { + Graph3d.prototype._redrawLegend = function () { var y; - if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE) { - + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { var dotSize = this.frame.clientWidth * 0.02; var widthMin, widthMax; if (this.style === Graph3d.STYLE.DOTSIZE) { widthMin = dotSize / 2; // px widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function - } - else { + } else { widthMin = 20; // px widthMax = 20; // px } @@ -7271,9 +7167,9 @@ return /******/ (function(modules) { // webpackBootstrap } var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = canvas.getContext("2d"); ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options + ctx.font = "14px arial"; // TODO: put in options if (this.style === Graph3d.STYLE.DOTCOLOR) { // draw the color bar @@ -7293,14 +7189,14 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); } - ctx.strokeStyle = this.colorAxis; + ctx.strokeStyle = this.colorAxis; ctx.strokeRect(left, top, widthMax, height); } if (this.style === Graph3d.STYLE.DOTSIZE) { // draw border around color bar - ctx.strokeStyle = this.colorAxis; - ctx.fillStyle = this.colorDot; + ctx.strokeStyle = this.colorAxis; + ctx.fillStyle = this.colorDot; ctx.beginPath(); ctx.moveTo(left, top); ctx.lineTo(right, top); @@ -7311,11 +7207,10 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); } - if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE) { + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { // print values along the color bar var gridLineLen = 5; // px - var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true); + var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); step.start(); if (step.getCurrent() < this.valueMin) { step.next(); @@ -7328,16 +7223,16 @@ return /******/ (function(modules) { // webpackBootstrap ctx.lineTo(left, y); ctx.stroke(); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; ctx.fillStyle = this.colorAxis; ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); step.next(); } - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; + ctx.textAlign = "right"; + ctx.textBaseline = "top"; var label = this.legendLabel; ctx.fillText(label, right, bottom + this.margin); } @@ -7346,18 +7241,18 @@ return /******/ (function(modules) { // webpackBootstrap /** * Redraw the filter */ - Graph3d.prototype._redrawFilter = function() { - this.frame.filter.innerHTML = ''; + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ""; if (this.dataFilter) { var options = { - 'visible': this.showAnimationControls + visible: this.showAnimationControls }; var slider = new Slider(this.frame.filter, options); this.frame.filter.slider = slider; // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; + this.frame.filter.style.padding = "10px"; //this.frame.filter.style.backgroundColor = '#EFEFEF'; slider.setValues(this.dataFilter.values); @@ -7374,8 +7269,7 @@ return /******/ (function(modules) { // webpackBootstrap me.redraw(); }; slider.setOnChangeCallback(onchange); - } - else { + } else { this.frame.filter.slider = undefined; } }; @@ -7383,8 +7277,8 @@ return /******/ (function(modules) { // webpackBootstrap /** * Redraw the slider */ - Graph3d.prototype._redrawSlider = function() { - if ( this.frame.filter.slider !== undefined) { + Graph3d.prototype._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { this.frame.filter.slider.redraw(); } }; @@ -7393,20 +7287,20 @@ return /******/ (function(modules) { // webpackBootstrap /** * Redraw common information */ - Graph3d.prototype._redrawInfo = function() { + Graph3d.prototype._redrawInfo = function () { if (this.dataFilter) { var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = canvas.getContext("2d"); - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + ctx.font = "14px arial"; // TODO: put in options + ctx.lineStyle = "gray"; + ctx.fillStyle = "gray"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; var x = this.margin; var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); + ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); } }; @@ -7414,17 +7308,26 @@ return /******/ (function(modules) { // webpackBootstrap /** * Redraw the axis */ - Graph3d.prototype._redrawAxis = function() { + Graph3d.prototype._redrawAxis = function () { var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - from, to, step, prettyStep, - text, xText, yText, zText, - offset, xOffset, yOffset, - xMin2d, xMax2d; + ctx = canvas.getContext("2d"), + from, + to, + step, + prettyStep, + text, + xText, + yText, + zText, + offset, + xOffset, + yOffset, + xMin2d, + xMax2d; // TODO: get the actual rendered style of the containerElement //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + ctx.font = 24 / this.camera.getArmLength() + "px arial"; // calculate the length for the short grid lines var gridLenX = 0.025 / this.scale.x; @@ -7434,7 +7337,7 @@ return /******/ (function(modules) { // webpackBootstrap // draw x-grid lines ctx.lineWidth = 1; - prettyStep = (this.defaultXStep === undefined); + prettyStep = this.defaultXStep === undefined; step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); step.start(); if (step.getCurrent() < this.xMin) { @@ -7451,10 +7354,9 @@ return /******/ (function(modules) { // webpackBootstrap ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); - } - else { + } else { from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); @@ -7462,7 +7364,7 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); @@ -7470,30 +7372,28 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); } - yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax; + yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax; text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; text.y += textMargin; - } - else if (Math.sin(armAngle * 2) < 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + } else if (Math.sin(armAngle * 2) < 0) { + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; } ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); + ctx.fillText(" " + this.xValueLabel(step.getCurrent()) + " ", text.x, text.y); step.next(); } // draw y-grid lines ctx.lineWidth = 1; - prettyStep = (this.defaultYStep === undefined); + prettyStep = this.defaultYStep === undefined; step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); step.start(); if (step.getCurrent() < this.yMin) { @@ -7508,10 +7408,9 @@ return /******/ (function(modules) { // webpackBootstrap ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); - } - else { + } else { from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); @@ -7519,7 +7418,7 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin)); ctx.strokeStyle = this.colorAxis; ctx.beginPath(); ctx.moveTo(from.x, from.y); @@ -7527,37 +7426,35 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); } - xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax; + xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax; text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; text.y += textMargin; - } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + } else if (Math.sin(armAngle * 2) > 0) { + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; } ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); + ctx.fillText(" " + this.yValueLabel(step.getCurrent()) + " ", text.x, text.y); step.next(); } // draw z-grid lines and axis ctx.lineWidth = 1; - prettyStep = (this.defaultZStep === undefined); + prettyStep = this.defaultZStep === undefined; step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); step.start(); if (step.getCurrent() < this.zMin) { step.next(); } - xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; - yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; + yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; while (!step.end()) { // TODO: make z-grid lines really 3d? from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); @@ -7567,10 +7464,10 @@ return /******/ (function(modules) { // webpackBootstrap ctx.lineTo(from.x - textMargin, from.y); ctx.stroke(); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; ctx.fillStyle = this.colorAxis; - ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); + ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); step.next(); } @@ -7626,19 +7523,17 @@ return /******/ (function(modules) { // webpackBootstrap if (xLabel.length > 0) { yOffset = 0.1 / this.scale.y; xText = (this.xMin + this.xMax) / 2; - yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset; + yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset; text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } - else if (Math.sin(armAngle * 2) < 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + } else if (Math.sin(armAngle * 2) < 0) { + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; } ctx.fillStyle = this.colorAxis; ctx.fillText(xLabel, text.x, text.y); @@ -7648,20 +7543,18 @@ return /******/ (function(modules) { // webpackBootstrap var yLabel = this.yLabel; if (yLabel.length > 0) { xOffset = 0.1 / this.scale.x; - xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset; + xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; yText = (this.yMin + this.yMax) / 2; text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + } else if (Math.sin(armAngle * 2) > 0) { + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; } ctx.fillStyle = this.colorAxis; ctx.fillText(yLabel, text.x, text.y); @@ -7670,13 +7563,13 @@ return /******/ (function(modules) { // webpackBootstrap // draw z-label var zLabel = this.zLabel; if (zLabel.length > 0) { - offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; - yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? + xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; + yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; zText = (this.zMin + this.zMax) / 2; text = this._convert3Dto2D(new Point3d(xText, yText, zText)); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; ctx.fillStyle = this.colorAxis; ctx.fillText(zLabel, text.x - offset, text.y); } @@ -7688,25 +7581,32 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} S Saturation, a value between 0 and 1 * @param {Number} V Value, a value between 0 and 1 */ - Graph3d.prototype._hsv2rgb = function(H, S, V) { + Graph3d.prototype._hsv2rgb = function (H, S, V) { var R, G, B, C, Hi, X; C = V * S; - Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(((H/60) % 2) - 1)); + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); switch (Hi) { - case 0: R = C; G = X; B = 0; break; - case 1: R = X; G = C; B = 0; break; - case 2: R = 0; G = C; B = X; break; - case 3: R = 0; G = X; B = C; break; - case 4: R = X; G = 0; B = C; break; - case 5: R = C; G = 0; B = X; break; + case 0: + R = C;G = X;B = 0;break; + case 1: + R = X;G = C;B = 0;break; + case 2: + R = 0;G = C;B = X;break; + case 3: + R = 0;G = X;B = C;break; + case 4: + R = X;G = 0;B = C;break; + case 5: + R = C;G = 0;B = X;break; - default: R = 0; G = 0; B = 0; break; + default: + R = 0;G = 0;B = 0;break; } - return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')'; + return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; }; @@ -7714,17 +7614,25 @@ return /******/ (function(modules) { // webpackBootstrap * Draw all datapoints as a grid * This function can be used when the style is 'grid' */ - Graph3d.prototype._redrawDataGrid = function() { + Graph3d.prototype._redrawDataGrid = function () { var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, right, top, cross, - i, - topSideVisible, fillStyle, strokeStyle, lineWidth, - h, s, v, zAvg; + ctx = canvas.getContext("2d"), + point, + right, + top, + cross, + i, + topSideVisible, + fillStyle, + strokeStyle, + lineWidth, + h, + s, + v, + zAvg; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations and screen position of all points for (i = 0; i < this.dataPoints.length; i++) { @@ -7749,11 +7657,10 @@ return /******/ (function(modules) { // webpackBootstrap for (i = 0; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; + top = this.dataPoints[i].pointTop; cross = this.dataPoints[i].pointCross; if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { - if (this.showGrayBottom || this.showShadow) { // calculate the cross product of the two vectors from center // to left and right, in order to know whether we are looking at the @@ -7765,31 +7672,28 @@ return /******/ (function(modules) { // webpackBootstrap var len = crossproduct.length(); // FIXME: there is a bug with determining the surface side (shadow or colored) - topSideVisible = (crossproduct.z > 0); - } - else { + topSideVisible = crossproduct.z > 0; + } else { topSideVisible = true; } if (topSideVisible) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; s = 1; // saturation if (this.showShadow) { - v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale + v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale fillStyle = this._hsv2rgb(h, s, v); strokeStyle = fillStyle; - } - else { + } else { v = 1; fillStyle = this._hsv2rgb(h, s, v); strokeStyle = this.colorAxis; } - } - else { - fillStyle = 'gray'; + } else { + fillStyle = "gray"; strokeStyle = this.colorAxis; } lineWidth = 0.5; @@ -7807,18 +7711,17 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); } } - } - else { // grid style + } else { + // grid style for (i = 0; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; + top = this.dataPoints[i].pointTop; if (point !== undefined) { if (this.showPerspective) { lineWidth = 2 / -point.trans.z; - } - else { + } else { lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); } } @@ -7826,7 +7729,7 @@ return /******/ (function(modules) { // webpackBootstrap if (point !== undefined && right !== undefined) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 zAvg = (point.point.z + right.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; ctx.lineWidth = lineWidth; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); @@ -7839,7 +7742,7 @@ return /******/ (function(modules) { // webpackBootstrap if (point !== undefined && top !== undefined) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; ctx.lineWidth = lineWidth; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); @@ -7857,13 +7760,12 @@ return /******/ (function(modules) { // webpackBootstrap * Draw all datapoints as dots. * This function can be used when the style is 'dot' or 'dot-line' */ - Graph3d.prototype._redrawDataDot = function() { + Graph3d.prototype._redrawDataDot = function () { var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = canvas.getContext("2d"); var i; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations of all points for (i = 0; i < this.dataPoints.length; i++) { @@ -7884,7 +7786,7 @@ return /******/ (function(modules) { // webpackBootstrap this.dataPoints.sort(sortDepth); // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * 0.02; // px + var dotSize = this.frame.clientWidth * 0.02; // px for (i = 0; i < this.dataPoints.length; i++) { var point = this.dataPoints[i]; @@ -7903,17 +7805,15 @@ return /******/ (function(modules) { // webpackBootstrap // calculate radius for the circle var size; if (this.style === Graph3d.STYLE.DOTSIZE) { - size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); - } - else { + size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + } else { size = dotSize; } var radius; if (this.showPerspective) { radius = size / -point.trans.z; - } - else { + } else { radius = size * -(this.eye.z / this.camera.getArmLength()); } if (radius < 0) { @@ -7921,29 +7821,27 @@ return /******/ (function(modules) { // webpackBootstrap } var hue, color, borderColor; - if (this.style === Graph3d.STYLE.DOTCOLOR ) { + if (this.style === Graph3d.STYLE.DOTCOLOR) { // calculate the color based on the value hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.DOTSIZE) { + } else if (this.style === Graph3d.STYLE.DOTSIZE) { color = this.colorDot; borderColor = this.colorDotBorder; - } - else { + } else { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); } // draw the circle - ctx.lineWidth = 1.0; + ctx.lineWidth = 1; ctx.strokeStyle = borderColor; ctx.fillStyle = color; ctx.beginPath(); - ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true); + ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true); ctx.fill(); ctx.stroke(); } @@ -7953,13 +7851,12 @@ return /******/ (function(modules) { // webpackBootstrap * Draw all datapoints as bars. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - Graph3d.prototype._redrawDataBar = function() { + Graph3d.prototype._redrawDataBar = function () { var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + var ctx = canvas.getContext("2d"); var i, j, surface, corners; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations of all points for (i = 0; i < this.dataPoints.length; i++) { @@ -7987,44 +7884,32 @@ return /******/ (function(modules) { // webpackBootstrap // determine color var hue, color, borderColor; - if (this.style === Graph3d.STYLE.BARCOLOR ) { + if (this.style === Graph3d.STYLE.BARCOLOR) { // calculate the color based on the value hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.BARSIZE) { + } else if (this.style === Graph3d.STYLE.BARSIZE) { color = this.colorDot; borderColor = this.colorDotBorder; - } - else { + } else { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; color = this._hsv2rgb(hue, 1, 1); borderColor = this._hsv2rgb(hue, 1, 0.8); } // calculate size for the bar if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); } // calculate all corner points var me = this; var point3d = point.point; - var top = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} - ]; - var bottom = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} - ]; + var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }]; + var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }]; // calculate screen location of the points top.forEach(function (obj) { @@ -8035,13 +7920,7 @@ return /******/ (function(modules) { // webpackBootstrap }); // create five sides, calculate both corner points and center points - var surfaces = [ - {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, - {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, - {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, - {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, - {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} - ]; + var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }]; point.surfaces = surfaces; // calculate the distance of each of the surface centers to the camera @@ -8092,13 +7971,13 @@ return /******/ (function(modules) { // webpackBootstrap * Draw a line through all datapoints. * This function can be used when the style is 'line' */ - Graph3d.prototype._redrawDataLine = function() { + Graph3d.prototype._redrawDataLine = function () { var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, i; + ctx = canvas.getContext("2d"), + point, + i; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? // calculate the translations of all points for (i = 0; i < this.dataPoints.length; i++) { @@ -8113,8 +7992,8 @@ return /******/ (function(modules) { // webpackBootstrap if (this.dataPoints.length > 0) { point = this.dataPoints[0]; - ctx.lineWidth = 1; // TODO: make customizable - ctx.strokeStyle = 'blue'; // TODO: make customizable + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = "blue"; // TODO: make customizable ctx.beginPath(); ctx.moveTo(point.screen.x, point.screen.y); } @@ -8136,7 +8015,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event The event that occurred (required for * retrieving the mouse position) */ - Graph3d.prototype._onMouseDown = function(event) { + Graph3d.prototype._onMouseDown = function (event) { event = event || window.event; // check if mouse is still down (may be up when focus is lost for example @@ -8146,7 +8025,7 @@ return /******/ (function(modules) { // webpackBootstrap } // only react on left mouse button down - this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; if (!this.leftButtonDown && !this.touchDown) return; // get mouse position (different code for IE and all other browsers) @@ -8157,16 +8036,20 @@ return /******/ (function(modules) { // webpackBootstrap this.startEnd = new Date(this.end); this.startArmRotation = this.camera.getArmRotation(); - this.frame.style.cursor = 'move'; + this.frame.style.cursor = "move"; // add event listeners to handle moving the contents // we store the function onmousemove and onmouseup in the graph, so we can // remove the eventlisteners lateron in the function mouseUp() var me = this; - this.onmousemove = function (event) {me._onMouseMove(event);}; - this.onmouseup = function (event) {me._onMouseUp(event);}; - util.addEventListener(document, 'mousemove', me.onmousemove); - util.addEventListener(document, 'mouseup', me.onmouseup); + this.onmousemove = function (event) { + me._onMouseMove(event); + }; + this.onmouseup = function (event) { + me._onMouseUp(event); + }; + util.addEventListener(document, "mousemove", me.onmousemove); + util.addEventListener(document, "mouseup", me.onmouseup); util.preventDefault(event); }; @@ -8192,18 +8075,18 @@ return /******/ (function(modules) { // webpackBootstrap // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... // the -0.001 is to take care that the vertical axis is always drawn at the left front corner if (Math.abs(Math.sin(horizontalNew)) < snapValue) { - horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001; + horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001; } if (Math.abs(Math.cos(horizontalNew)) < snapValue) { - horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001; + horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; } // snap vertically to nice angles if (Math.abs(Math.sin(verticalNew)) < snapValue) { - verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI; + verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; } if (Math.abs(Math.cos(verticalNew)) < snapValue) { - verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI; + verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; } this.camera.setArmRotation(horizontalNew, verticalNew); @@ -8211,7 +8094,7 @@ return /******/ (function(modules) { // webpackBootstrap // fire a cameraPositionChange event var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + this.emit("cameraPositionChange", parameters); util.preventDefault(event); }; @@ -8223,12 +8106,12 @@ return /******/ (function(modules) { // webpackBootstrap * @param {event} event The event */ Graph3d.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; + this.frame.style.cursor = "auto"; this.leftButtonDown = false; // remove event listeners here - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); util.preventDefault(event); }; @@ -8263,13 +8146,11 @@ return /******/ (function(modules) { // webpackBootstrap // datapoint changed if (dataPoint) { this._showTooltip(dataPoint); - } - else { + } else { this._hideTooltip(); } } - } - else { + } else { // tooltip is currently not visible var me = this; this.tooltipTimeout = setTimeout(function () { @@ -8287,14 +8168,18 @@ return /******/ (function(modules) { // webpackBootstrap /** * Event handler for touchstart event on mobile devices */ - Graph3d.prototype._onTouchStart = function(event) { + Graph3d.prototype._onTouchStart = function (event) { this.touchDown = true; var me = this; - this.ontouchmove = function (event) {me._onTouchMove(event);}; - this.ontouchend = function (event) {me._onTouchEnd(event);}; - util.addEventListener(document, 'touchmove', me.ontouchmove); - util.addEventListener(document, 'touchend', me.ontouchend); + this.ontouchmove = function (event) { + me._onTouchMove(event); + }; + this.ontouchend = function (event) { + me._onTouchEnd(event); + }; + util.addEventListener(document, "touchmove", me.ontouchmove); + util.addEventListener(document, "touchend", me.ontouchend); this._onMouseDown(event); }; @@ -8302,18 +8187,18 @@ return /******/ (function(modules) { // webpackBootstrap /** * Event handler for touchmove event on mobile devices */ - Graph3d.prototype._onTouchMove = function(event) { + Graph3d.prototype._onTouchMove = function (event) { this._onMouseMove(event); }; /** * Event handler for touchend event on mobile devices */ - Graph3d.prototype._onTouchEnd = function(event) { + Graph3d.prototype._onTouchEnd = function (event) { this.touchDown = false; - util.removeEventListener(document, 'touchmove', this.ontouchmove); - util.removeEventListener(document, 'touchend', this.ontouchend); + util.removeEventListener(document, "touchmove", this.ontouchmove); + util.removeEventListener(document, "touchend", this.ontouchend); this._onMouseUp(event); }; @@ -8324,18 +8209,20 @@ return /******/ (function(modules) { // webpackBootstrap * Code from http://adomas.org/javascript-mouse-wheel/ * @param {event} event The event */ - Graph3d.prototype._onWheel = function(event) { + Graph3d.prototype._onWheel = function (event) { if (!event) /* For IE. */ event = window.event; // retrieve delta var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta/120; - } else if (event.detail) { /* Mozilla case. */ + 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; + delta = -event.detail / 3; } // If delta is nonzero, handle it. @@ -8353,7 +8240,7 @@ return /******/ (function(modules) { // webpackBootstrap // fire a cameraPositionChange event var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + this.emit("cameraPositionChange", parameters); // Prevent default actions caused by mouse wheel. // That might be ugly, but we handle scrolls somehow @@ -8370,10 +8257,10 @@ return /******/ (function(modules) { // webpackBootstrap */ Graph3d.prototype._insideTriangle = function (point, triangle) { var a = triangle[0], - b = triangle[1], - c = triangle[2]; + b = triangle[1], + c = triangle[2]; - function sign (x) { + function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; } @@ -8382,9 +8269,7 @@ return /******/ (function(modules) { // webpackBootstrap var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); // each of the three signs must be either equal to each other or zero - return (as == 0 || bs == 0 || as == bs) && - (bs == 0 || cs == 0 || bs == cs) && - (as == 0 || cs == 0 || as == cs); + return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs); }; /** @@ -8396,19 +8281,18 @@ return /******/ (function(modules) { // webpackBootstrap */ Graph3d.prototype._dataPointFromXY = function (x, y) { var i, - distMax = 100, // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); - - if (this.style === Graph3d.STYLE.BAR || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); + + if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { // the data points are ordered from far away to closest for (i = this.dataPoints.length - 1; i >= 0; i--) { dataPoint = this.dataPoints[i]; - var surfaces = dataPoint.surfaces; + var surfaces = dataPoint.surfaces; if (surfaces) { for (var s = surfaces.length - 1; s >= 0; s--) { // split each surface in two triangles, and see if the center point is inside one of these @@ -8416,16 +8300,14 @@ return /******/ (function(modules) { // webpackBootstrap var corners = surface.corners; var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; - if (this._insideTriangle(center, triangle1) || - this._insideTriangle(center, triangle2)) { + if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) { // return immediately at the first hit return dataPoint; } } } } - } - else { + } else { // find the closest data point, using distance to the center of the point on 2d screen for (i = 0; i < this.dataPoints.length; i++) { dataPoint = this.dataPoints[i]; @@ -8433,7 +8315,7 @@ return /******/ (function(modules) { // webpackBootstrap if (point) { var distX = Math.abs(x - point.x); var distY = Math.abs(y - point.y); - var dist = Math.sqrt(distX * distX + distY * distY); + var dist = Math.sqrt(distX * distX + distY * distY); if ((closestDist === null || dist < closestDist) && dist < distMax) { closestDist = dist; @@ -8456,27 +8338,27 @@ return /******/ (function(modules) { // webpackBootstrap var content, line, dot; if (!this.tooltip) { - content = document.createElement('div'); - content.style.position = 'absolute'; - content.style.padding = '10px'; - content.style.border = '1px solid #4d4d4d'; - content.style.color = '#1a1a1a'; - content.style.background = 'rgba(255,255,255,0.7)'; - content.style.borderRadius = '2px'; - content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; - - line = document.createElement('div'); - line.style.position = 'absolute'; - line.style.height = '40px'; - line.style.width = '0'; - line.style.borderLeft = '1px solid #4d4d4d'; - - dot = document.createElement('div'); - dot.style.position = 'absolute'; - dot.style.height = '0'; - dot.style.width = '0'; - dot.style.border = '5px solid #4d4d4d'; - dot.style.borderRadius = '5px'; + content = document.createElement("div"); + content.style.position = "absolute"; + content.style.padding = "10px"; + content.style.border = "1px solid #4d4d4d"; + content.style.color = "#1a1a1a"; + content.style.background = "rgba(255,255,255,0.7)"; + content.style.borderRadius = "2px"; + content.style.boxShadow = "5px 5px 10px rgba(128,128,128,0.5)"; + + line = document.createElement("div"); + line.style.position = "absolute"; + line.style.height = "40px"; + line.style.width = "0"; + line.style.borderLeft = "1px solid #4d4d4d"; + + dot = document.createElement("div"); + dot.style.position = "absolute"; + dot.style.height = "0"; + dot.style.width = "0"; + dot.style.border = "5px solid #4d4d4d"; + dot.style.borderRadius = "5px"; this.tooltip = { dataPoint: null, @@ -8486,49 +8368,43 @@ return /******/ (function(modules) { // webpackBootstrap dot: dot } }; - } - else { + } else { content = this.tooltip.dom.content; - line = this.tooltip.dom.line; - dot = this.tooltip.dom.dot; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; } this._hideTooltip(); this.tooltip.dataPoint = dataPoint; - if (typeof this.showTooltip === 'function') { + if (typeof this.showTooltip === "function") { content.innerHTML = this.showTooltip(dataPoint.point); - } - else { - content.innerHTML = '' + - '' + - '' + - '' + - '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; + } else { + content.innerHTML = "" + "" + "" + "" + "
x:" + dataPoint.point.x + "
y:" + dataPoint.point.y + "
z:" + dataPoint.point.z + "
"; } - content.style.left = '0'; - content.style.top = '0'; + content.style.left = "0"; + content.style.top = "0"; this.frame.appendChild(content); this.frame.appendChild(line); this.frame.appendChild(dot); // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; var left = dataPoint.screen.x - contentWidth / 2; left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); - line.style.left = dataPoint.screen.x + 'px'; - line.style.top = (dataPoint.screen.y - lineHeight) + 'px'; - content.style.left = left + 'px'; - content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px'; - dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px'; - dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px'; + line.style.left = dataPoint.screen.x + "px"; + line.style.top = dataPoint.screen.y - lineHeight + "px"; + content.style.left = left + "px"; + content.style.top = dataPoint.screen.y - lineHeight - contentHeight + "px"; + dot.style.left = dataPoint.screen.x - dotWidth / 2 + "px"; + dot.style.top = dataPoint.screen.y - dotHeight / 2 + "px"; }; /** @@ -8558,9 +8434,10 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @return {Number} mouse x */ - function getMouseX (event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + function getMouseX(event) { + if ("clientX" in event) { + return event.clientX; + }return event.targetTouches[0] && event.targetTouches[0].clientX || 0; } /** @@ -8568,13 +8445,14 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @return {Number} mouse y */ - function getMouseY (event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + function getMouseY(event) { + if ("clientY" in event) { + return event.clientY; + }return event.targetTouches[0] && event.targetTouches[0].clientY || 0; } module.exports = Graph3d; - + // use use defaults /***/ }, /* 11 */ @@ -8750,6 +8628,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 12 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * @prototype Point3d * @param {Number} [x] @@ -8768,7 +8648,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Point3d} b * @return {Point3d} a-b */ - Point3d.subtract = function(a, b) { + Point3d.subtract = function (a, b) { var sub = new Point3d(); sub.x = a.x - b.x; sub.y = a.y - b.y; @@ -8782,7 +8662,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Point3d} b * @return {Point3d} a+b */ - Point3d.add = function(a, b) { + Point3d.add = function (a, b) { var sum = new Point3d(); sum.x = a.x + b.x; sum.y = a.y + b.y; @@ -8796,12 +8676,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Point3d} b * @return {Point3d} The average, (a+b)/2 */ - Point3d.avg = function(a, b) { - return new Point3d( - (a.x + b.x) / 2, - (a.y + b.y) / 2, - (a.z + b.z) / 2 - ); + Point3d.avg = function (a, b) { + return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); }; /** @@ -8811,7 +8687,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Point3d} b * @return {Point3d} cross product axb */ - Point3d.crossProduct = function(a, b) { + Point3d.crossProduct = function (a, b) { var crossproduct = new Point3d(); crossproduct.x = a.y * b.z - a.z * b.y; @@ -8826,38 +8702,36 @@ return /******/ (function(modules) { // webpackBootstrap * Rtrieve the length of the vector (or the distance from this point to the origin * @return {Number} length */ - Point3d.prototype.length = function() { - return Math.sqrt( - this.x * this.x + - this.y * this.y + - this.z * this.z - ); + Point3d.prototype.length = function () { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); }; module.exports = Point3d; - /***/ }, /* 13 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * @prototype Point2d * @param {Number} [x] * @param {Number} [y] */ - function Point2d (x, y) { + function Point2d(x, y) { this.x = x !== undefined ? x : 0; this.y = y !== undefined ? y : 0; } module.exports = Point2d; - /***/ }, /* 14 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Point3d = __webpack_require__(12); /** @@ -8878,7 +8752,7 @@ return /******/ (function(modules) { // webpackBootstrap this.armLength = 1.7; this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); this.calculateCameraOrientation(); } @@ -8889,7 +8763,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} y Normalized value of y * @param {Number} z Normalized value of z */ - Camera.prototype.setArmLocation = function(x, y, z) { + Camera.prototype.setArmLocation = function (x, y, z) { this.armLocation.x = x; this.armLocation.y = y; this.armLocation.z = z; @@ -8905,7 +8779,7 @@ return /******/ (function(modules) { // webpackBootstrap * if vertical=0.5*PI, the graph is shown from the * top. Optional, can be left undefined. */ - Camera.prototype.setArmRotation = function(horizontal, vertical) { + Camera.prototype.setArmRotation = function (horizontal, vertical) { if (horizontal !== undefined) { this.armRotation.horizontal = horizontal; } @@ -8913,7 +8787,7 @@ return /******/ (function(modules) { // webpackBootstrap if (vertical !== undefined) { this.armRotation.vertical = vertical; if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; - if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; + if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI; } if (horizontal !== undefined || vertical !== undefined) { @@ -8925,7 +8799,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the current arm rotation * @return {object} An object with parameters horizontal and vertical */ - Camera.prototype.getArmRotation = function() { + Camera.prototype.getArmRotation = function () { var rot = {}; rot.horizontal = this.armRotation.horizontal; rot.vertical = this.armRotation.vertical; @@ -8937,9 +8811,8 @@ return /******/ (function(modules) { // webpackBootstrap * Set the (normalized) length of the camera arm. * @param {Number} length A length between 0.71 and 5.0 */ - Camera.prototype.setArmLength = function(length) { - if (length === undefined) - return; + Camera.prototype.setArmLength = function (length) { + if (length === undefined) return; this.armLength = length; @@ -8947,7 +8820,7 @@ return /******/ (function(modules) { // webpackBootstrap // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the // graph if (this.armLength < 0.71) this.armLength = 0.71; - if (this.armLength > 5.0) this.armLength = 5.0; + if (this.armLength > 5) this.armLength = 5; this.calculateCameraOrientation(); }; @@ -8956,7 +8829,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the arm length * @return {Number} length */ - Camera.prototype.getArmLength = function() { + Camera.prototype.getArmLength = function () { return this.armLength; }; @@ -8964,7 +8837,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the camera location * @return {Point3d} cameraLocation */ - Camera.prototype.getCameraLocation = function() { + Camera.prototype.getCameraLocation = function () { return this.cameraLocation; }; @@ -8972,7 +8845,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the camera rotation * @return {Point3d} cameraRotation */ - Camera.prototype.getCameraRotation = function() { + Camera.prototype.getCameraRotation = function () { return this.cameraRotation; }; @@ -8980,14 +8853,14 @@ return /******/ (function(modules) { // webpackBootstrap * Calculate the location and rotation of the camera based on the * position and orientation of the camera arm */ - Camera.prototype.calculateCameraOrientation = function() { + Camera.prototype.calculateCameraOrientation = function () { // calculate location of the camera this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); // calculate rotation of the camera - this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; + this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; this.cameraRotation.y = 0; this.cameraRotation.z = -this.armRotation.horizontal; }; @@ -8998,6 +8871,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 15 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var DataView = __webpack_require__(9); /** @@ -9007,7 +8882,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} column The index of the column to be filtered * @param {Graph} graph The graph */ - function Filter (data, column, graph) { + function Filter(data, column, graph) { this.data = data; this.column = column; this.graph = graph; // the parent graph @@ -9036,8 +8911,7 @@ return /******/ (function(modules) { // webpackBootstrap if (graph.animationPreload) { this.loaded = false; this.loadInBackground(); - } - else { + } else { this.loaded = true; } }; @@ -9047,7 +8921,7 @@ return /******/ (function(modules) { // webpackBootstrap * Return the label * @return {string} label */ - Filter.prototype.isLoaded = function() { + Filter.prototype.isLoaded = function () { return this.loaded; }; @@ -9056,7 +8930,7 @@ return /******/ (function(modules) { // webpackBootstrap * Return the loaded progress * @return {Number} percentage between 0 and 100 */ - Filter.prototype.getLoadedProgress = function() { + Filter.prototype.getLoadedProgress = function () { var len = this.values.length; var i = 0; @@ -9072,7 +8946,7 @@ return /******/ (function(modules) { // webpackBootstrap * Return the label * @return {string} label */ - Filter.prototype.getLabel = function() { + Filter.prototype.getLabel = function () { return this.graph.filterLabel; }; @@ -9081,7 +8955,7 @@ return /******/ (function(modules) { // webpackBootstrap * Return the columnIndex of the filter * @return {Number} columnIndex */ - Filter.prototype.getColumn = function() { + Filter.prototype.getColumn = function () { return this.column; }; @@ -9089,9 +8963,8 @@ return /******/ (function(modules) { // webpackBootstrap * Return the currently selected value. Returns undefined if there is no selection * @return {*} value */ - Filter.prototype.getSelectedValue = function() { - if (this.index === undefined) - return undefined; + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; return this.values[this.index]; }; @@ -9100,7 +8973,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve all values of the filter * @return {Array} values */ - Filter.prototype.getValues = function() { + Filter.prototype.getValues = function () { return this.values; }; @@ -9109,9 +8982,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} index * @return {*} value */ - Filter.prototype.getValue = function(index) { - if (index >= this.values.length) - throw 'Error: index out of range'; + Filter.prototype.getValue = function (index) { + if (index >= this.values.length) throw "Error: index out of range"; return this.values[index]; }; @@ -9122,23 +8994,22 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} [index] (optional) * @return {Array} dataPoints */ - Filter.prototype._getDataPoints = function(index) { - if (index === undefined) - index = this.index; + Filter.prototype._getDataPoints = function (index) { + if (index === undefined) index = this.index; - if (index === undefined) - return []; + if (index === undefined) return []; var dataPoints; if (this.dataPoints[index]) { dataPoints = this.dataPoints[index]; - } - else { + } else { var f = {}; f.column = this.column; f.value = this.values[index]; - var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get(); + var dataView = new DataView(this.data, { filter: function (item) { + return item[f.column] == f.value; + } }).get(); dataPoints = this.graph._getDataPoints(dataView); this.dataPoints[index] = dataPoints; @@ -9152,7 +9023,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Set a callback function when the filter is fully loaded. */ - Filter.prototype.setOnLoadCallback = function(callback) { + Filter.prototype.setOnLoadCallback = function (callback) { this.onLoadCallback = callback; }; @@ -9162,9 +9033,8 @@ return /******/ (function(modules) { // webpackBootstrap * No double entries will be created. * @param {Number} index */ - Filter.prototype.selectValue = function(index) { - if (index >= this.values.length) - throw 'Error: index out of range'; + Filter.prototype.selectValue = function (index) { + if (index >= this.values.length) throw "Error: index out of range"; this.index = index; this.value = this.values[index]; @@ -9174,9 +9044,8 @@ return /******/ (function(modules) { // webpackBootstrap * Load all filtered rows in the background one by one * Start this method without providing an index! */ - Filter.prototype.loadInBackground = function(index) { - if (index === undefined) - index = 0; + Filter.prototype.loadInBackground = function (index) { + if (index === undefined) index = 0; var frame = this.graph.frame; @@ -9186,22 +9055,23 @@ return /******/ (function(modules) { // webpackBootstrap // create a progress box if (frame.progress === undefined) { - frame.progress = document.createElement('DIV'); - frame.progress.style.position = 'absolute'; - frame.progress.style.color = 'gray'; + frame.progress = document.createElement("DIV"); + frame.progress.style.position = "absolute"; + frame.progress.style.color = "gray"; frame.appendChild(frame.progress); } var progress = this.getLoadedProgress(); - frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; + frame.progress.innerHTML = "Loading animation... " + progress + "%"; // TODO: this is no nice solution... - frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider - frame.progress.style.left = 10 + 'px'; + frame.progress.style.bottom = 60 + "px"; // TODO: use height of slider + frame.progress.style.left = 10 + "px"; var me = this; - setTimeout(function() {me.loadInBackground(index+1);}, 10); + setTimeout(function () { + me.loadInBackground(index + 1); + }, 10); this.loaded = false; - } - else { + } else { this.loaded = true; // remove the progress box @@ -9210,18 +9080,18 @@ return /******/ (function(modules) { // webpackBootstrap frame.progress = undefined; } - if (this.onLoadCallback) - this.onLoadCallback(); + if (this.onLoadCallback) this.onLoadCallback(); } }; module.exports = Filter; - /***/ }, /* 16 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); /** @@ -9235,59 +9105,67 @@ return /******/ (function(modules) { // webpackBootstrap */ function Slider(container, options) { if (container === undefined) { - throw 'Error: No container element defined'; + throw "Error: No container element defined"; } this.container = container; - this.visible = (options && options.visible != undefined) ? options.visible : true; + this.visible = options && options.visible != undefined ? options.visible : true; if (this.visible) { - this.frame = document.createElement('DIV'); + this.frame = document.createElement("DIV"); //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = '100%'; - this.frame.style.position = 'relative'; + this.frame.style.width = "100%"; + this.frame.style.position = "relative"; this.container.appendChild(this.frame); - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; this.frame.appendChild(this.frame.prev); - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; this.frame.appendChild(this.frame.play); - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; this.frame.appendChild(this.frame.next); - this.frame.bar = document.createElement('INPUT'); - this.frame.bar.type = 'BUTTON'; - this.frame.bar.style.position = 'absolute'; - this.frame.bar.style.border = '1px solid red'; - this.frame.bar.style.width = '100px'; - this.frame.bar.style.height = '6px'; - this.frame.bar.style.borderRadius = '2px'; - this.frame.bar.style.MozBorderRadius = '2px'; - this.frame.bar.style.border = '1px solid #7F7F7F'; - this.frame.bar.style.backgroundColor = '#E5E5E5'; + this.frame.bar = document.createElement("INPUT"); + this.frame.bar.type = "BUTTON"; + this.frame.bar.style.position = "absolute"; + this.frame.bar.style.border = "1px solid red"; + this.frame.bar.style.width = "100px"; + this.frame.bar.style.height = "6px"; + this.frame.bar.style.borderRadius = "2px"; + this.frame.bar.style.MozBorderRadius = "2px"; + this.frame.bar.style.border = "1px solid #7F7F7F"; + this.frame.bar.style.backgroundColor = "#E5E5E5"; this.frame.appendChild(this.frame.bar); - this.frame.slide = document.createElement('INPUT'); - this.frame.slide.type = 'BUTTON'; - this.frame.slide.style.margin = '0px'; - this.frame.slide.value = ' '; - this.frame.slide.style.position = 'relative'; - this.frame.slide.style.left = '-100px'; + this.frame.slide = document.createElement("INPUT"); + this.frame.slide.type = "BUTTON"; + this.frame.slide.style.margin = "0px"; + this.frame.slide.value = " "; + this.frame.slide.style.position = "relative"; + this.frame.slide.style.left = "-100px"; this.frame.appendChild(this.frame.slide); // create events var me = this; - this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; - this.frame.prev.onclick = function (event) {me.prev(event);}; - this.frame.play.onclick = function (event) {me.togglePlay(event);}; - this.frame.next.onclick = function (event) {me.next(event);}; + this.frame.slide.onmousedown = function (event) { + me._onMouseDown(event); + }; + this.frame.prev.onclick = function (event) { + me.prev(event); + }; + this.frame.play.onclick = function (event) { + me.togglePlay(event); + }; + this.frame.next.onclick = function (event) { + me.next(event); + }; } this.onChangeCallback = undefined; @@ -9303,7 +9181,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Select the previous index */ - Slider.prototype.prev = function() { + Slider.prototype.prev = function () { var index = this.getIndex(); if (index > 0) { index--; @@ -9314,7 +9192,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Select the next index */ - Slider.prototype.next = function() { + Slider.prototype.next = function () { var index = this.getIndex(); if (index < this.values.length - 1) { index++; @@ -9325,22 +9203,21 @@ return /******/ (function(modules) { // webpackBootstrap /** * Select the next index */ - Slider.prototype.playNext = function() { + Slider.prototype.playNext = function () { var start = new Date(); var index = this.getIndex(); if (index < this.values.length - 1) { index++; this.setIndex(index); - } - else if (this.playLoop) { + } else if (this.playLoop) { // jump to the start index = 0; this.setIndex(index); } var end = new Date(); - var diff = (end - start); + var diff = end - start; // calculate how much time it to to set the index and to execute the callback // function. @@ -9348,13 +9225,15 @@ return /******/ (function(modules) { // webpackBootstrap // document.title = diff // TODO: cleanup var me = this; - this.playTimeout = setTimeout(function() {me.playNext();}, interval); + this.playTimeout = setTimeout(function () { + me.playNext(); + }, interval); }; /** * Toggle start or stop playing */ - Slider.prototype.togglePlay = function() { + Slider.prototype.togglePlay = function () { if (this.playTimeout === undefined) { this.play(); } else { @@ -9365,26 +9244,26 @@ return /******/ (function(modules) { // webpackBootstrap /** * Start playing */ - Slider.prototype.play = function() { + Slider.prototype.play = function () { // Test whether already playing if (this.playTimeout) return; this.playNext(); if (this.frame) { - this.frame.play.value = 'Stop'; + this.frame.play.value = "Stop"; } }; /** * Stop playing */ - Slider.prototype.stop = function() { + Slider.prototype.stop = function () { clearInterval(this.playTimeout); this.playTimeout = undefined; if (this.frame) { - this.frame.play.value = 'Play'; + this.frame.play.value = "Play"; } }; @@ -9392,7 +9271,7 @@ return /******/ (function(modules) { // webpackBootstrap * Set a callback function which will be triggered when the value of the * slider bar has changed. */ - Slider.prototype.setOnChangeCallback = function(callback) { + Slider.prototype.setOnChangeCallback = function (callback) { this.onChangeCallback = callback; }; @@ -9400,7 +9279,7 @@ return /******/ (function(modules) { // webpackBootstrap * Set the interval for playing the list * @param {Number} interval The interval in milliseconds */ - Slider.prototype.setPlayInterval = function(interval) { + Slider.prototype.setPlayInterval = function (interval) { this.playInterval = interval; }; @@ -9408,7 +9287,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the current play interval * @return {Number} interval The interval in milliseconds */ - Slider.prototype.getPlayInterval = function(interval) { + Slider.prototype.getPlayInterval = function (interval) { return this.playInterval; }; @@ -9418,7 +9297,7 @@ return /******/ (function(modules) { // webpackBootstrap * the end is passed, and will jump to the end * when the start is passed. */ - Slider.prototype.setPlayLoop = function(doLoop) { + Slider.prototype.setPlayLoop = function (doLoop) { this.playLoop = doLoop; }; @@ -9426,7 +9305,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Execute the onchange callback function */ - Slider.prototype.onChange = function() { + Slider.prototype.onChange = function () { if (this.onChangeCallback !== undefined) { this.onChangeCallback(); } @@ -9435,19 +9314,15 @@ return /******/ (function(modules) { // webpackBootstrap /** * redraw the slider on the correct place */ - Slider.prototype.redraw = function() { + Slider.prototype.redraw = function () { if (this.frame) { // resize the bar - this.frame.bar.style.top = (this.frame.clientHeight/2 - - this.frame.bar.offsetHeight/2) + 'px'; - this.frame.bar.style.width = (this.frame.clientWidth - - this.frame.prev.clientWidth - - this.frame.play.clientWidth - - this.frame.next.clientWidth - 30) + 'px'; + this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + "px"; + this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + "px"; // position the slider button var left = this.indexToLeft(this.index); - this.frame.slide.style.left = (left) + 'px'; + this.frame.slide.style.left = left + "px"; } }; @@ -9456,28 +9331,24 @@ return /******/ (function(modules) { // webpackBootstrap * Set the list with values for the slider * @param {Array} values A javascript array with values (any type) */ - Slider.prototype.setValues = function(values) { + Slider.prototype.setValues = function (values) { this.values = values; - if (this.values.length > 0) - this.setIndex(0); - else - this.index = undefined; + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; }; /** * Select a value by its index * @param {Number} index */ - Slider.prototype.setIndex = function(index) { + Slider.prototype.setIndex = function (index) { if (index < this.values.length) { this.index = index; this.redraw(); this.onChange(); - } - else { - throw 'Error: index out of range'; + } else { + throw "Error: index out of range"; } }; @@ -9485,7 +9356,7 @@ return /******/ (function(modules) { // webpackBootstrap * retrieve the index of the currently selected vaue * @return {Number} index */ - Slider.prototype.getIndex = function() { + Slider.prototype.getIndex = function () { return this.index; }; @@ -9494,50 +9365,52 @@ return /******/ (function(modules) { // webpackBootstrap * retrieve the currently selected value * @return {*} value */ - Slider.prototype.get = function() { + Slider.prototype.get = function () { return this.values[this.index]; }; - Slider.prototype._onMouseDown = function(event) { + Slider.prototype._onMouseDown = function (event) { // only react on left mouse button down - var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + var leftButtonDown = event.which ? event.which === 1 : event.button === 1; if (!leftButtonDown) return; this.startClientX = event.clientX; this.startSlideX = parseFloat(this.frame.slide.style.left); - this.frame.style.cursor = 'move'; + this.frame.style.cursor = "move"; // add event listeners to handle moving the contents // we store the function onmousemove and onmouseup in the graph, so we can // remove the eventlisteners lateron in the function mouseUp() var me = this; - this.onmousemove = function (event) {me._onMouseMove(event);}; - this.onmouseup = function (event) {me._onMouseUp(event);}; - util.addEventListener(document, 'mousemove', this.onmousemove); - util.addEventListener(document, 'mouseup', this.onmouseup); + this.onmousemove = function (event) { + me._onMouseMove(event); + }; + this.onmouseup = function (event) { + me._onMouseUp(event); + }; + util.addEventListener(document, "mousemove", this.onmousemove); + util.addEventListener(document, "mouseup", this.onmouseup); util.preventDefault(event); }; Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; var x = left - 3; - var index = Math.round(x / width * (this.values.length-1)); + var index = Math.round(x / width * (this.values.length - 1)); if (index < 0) index = 0; - if (index > this.values.length-1) index = this.values.length-1; + if (index > this.values.length - 1) index = this.values.length - 1; return index; }; Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - var x = index / (this.values.length-1) * width; + var x = index / (this.values.length - 1) * width; var left = x + 3; return left; @@ -9558,22 +9431,23 @@ return /******/ (function(modules) { // webpackBootstrap Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; + this.frame.style.cursor = "auto"; // remove event listeners - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); util.preventDefault(); }; module.exports = Slider; - /***/ }, /* 17 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * @prototype StepNumber * The class StepNumber is an iterator for Numbers. You provide a start and end @@ -9620,7 +9494,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {boolean} prettyStep Optional. If true, the step size is rounded * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - StepNumber.prototype.setRange = function(start, end, step, prettyStep) { + StepNumber.prototype.setRange = function (start, end, step, prettyStep) { this._start = start ? start : 0; this._end = end ? end : 0; @@ -9633,17 +9507,12 @@ return /******/ (function(modules) { // webpackBootstrap * @param {boolean} prettyStep Optional. If true, the provided step is rounded * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - StepNumber.prototype.setStep = function(step, prettyStep) { - if (step === undefined || step <= 0) - return; + StepNumber.prototype.setStep = function (step, prettyStep) { + if (step === undefined || step <= 0) return; - if (prettyStep !== undefined) - this.prettyStep = prettyStep; + if (prettyStep !== undefined) this.prettyStep = prettyStep; - if (this.prettyStep === true) - this._step = StepNumber.calculatePrettyStep(step); - else - this._step = step; + if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** @@ -9654,7 +9523,9 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Number} Nice step size */ StepNumber.calculatePrettyStep = function (step) { - var log10 = function (x) {return Math.log(x) / Math.LN10;}; + var log10 = function (x) { + return Math.log(x) / Math.LN10; + }; // try three steps (multiple of 1, 2, or 5 var step1 = Math.pow(10, Math.round(log10(step))), @@ -9694,7 +9565,7 @@ return /******/ (function(modules) { // webpackBootstrap * Set the current value to the largest value smaller than start, which * is a multiple of the step size */ - StepNumber.prototype.start = function() { + StepNumber.prototype.start = function () { this._current = this._start - this._start % this._step; }; @@ -9710,16 +9581,17 @@ return /******/ (function(modules) { // webpackBootstrap * @return {boolean} True if the current value has passed the end value. */ StepNumber.prototype.end = function () { - return (this._current > this._end); + return this._current > this._end; }; module.exports = StepNumber; - /***/ }, /* 18 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Emitter = __webpack_require__(11); var Hammer = __webpack_require__(19); var util = __webpack_require__(1); @@ -9741,9 +9613,9 @@ return /******/ (function(modules) { // webpackBootstrap * @constructor * @extends Core */ - function Timeline (container, items, groups, options) { + function Timeline(container, items, groups, options) { if (!(this instanceof Timeline)) { - throw new SyntaxError('Constructor must be called with the new operator'); + throw new SyntaxError("Constructor must be called with the new operator"); } // if the third element is options, the forth is groups (optionally); @@ -9756,11 +9628,11 @@ return /******/ (function(modules) { // webpackBootstrap var me = this; this.defaultOptions = { start: null, - end: null, + end: null, autoResize: true, - orientation: 'bottom', + orientation: "bottom", width: null, height: null, maxHeight: null, @@ -9794,7 +9666,7 @@ return /******/ (function(modules) { // webpackBootstrap toScreen: me._toScreen.bind(me), toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toTime: me._toTime.bind(me), - toGlobalTime : me._toGlobalTime.bind(me) + toGlobalTime: me._toGlobalTime.bind(me) } }; @@ -9820,8 +9692,8 @@ return /******/ (function(modules) { // webpackBootstrap this.itemSet = new ItemSet(this.body); this.components.push(this.itemSet); - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet // apply options if (options) { @@ -9836,8 +9708,7 @@ return /******/ (function(modules) { // webpackBootstrap // create itemset if (items) { this.setItems(items); - } - else { + } else { this._redraw(); } } @@ -9850,8 +9721,8 @@ return /******/ (function(modules) { // webpackBootstrap * Can be useful to manually redraw when option autoResize=false and the window * has been resized, or when the items CSS has been changed. */ - Timeline.prototype.redraw = function() { - this.itemSet && this.itemSet.markDirty({refreshItems: true}); + Timeline.prototype.redraw = function () { + this.itemSet && this.itemSet.markDirty({ refreshItems: true }); this._redraw(); }; @@ -9859,23 +9730,21 @@ return /******/ (function(modules) { // webpackBootstrap * Set items * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - Timeline.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; // convert to type DataSet when needed var newDataSet; if (!items) { newDataSet = null; - } - else if (items instanceof DataSet || items instanceof DataView) { + } else if (items instanceof DataSet || items instanceof DataView) { newDataSet = items; - } - else { + } else { // turn an array into a dataset newDataSet = new DataSet(items, { type: { - start: 'Date', - end: 'Date' + start: "Date", + end: "Date" } }); } @@ -9891,12 +9760,11 @@ return /******/ (function(modules) { // webpackBootstrap } var start = this.options.start != undefined ? this.options.start : dataRange.start; - var end = this.options.end != undefined ? this.options.end : dataRange.end; + var end = this.options.end != undefined ? this.options.end : dataRange.end; - this.setWindow(start, end, {animate: false}); - } - else { - this.fit({animate: false}); + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); } } }; @@ -9905,16 +9773,14 @@ return /******/ (function(modules) { // webpackBootstrap * Set groups * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - Timeline.prototype.setGroups = function(groups) { + Timeline.prototype.setGroups = function (groups) { // convert to type DataSet when needed var newDataSet; if (!groups) { newDataSet = null; - } - else if (groups instanceof DataSet || groups instanceof DataView) { + } else if (groups instanceof DataSet || groups instanceof DataView) { newDataSet = groups; - } - else { + } else { // turn an array into a dataset newDataSet = new DataSet(groups); } @@ -9939,7 +9805,7 @@ return /******/ (function(modules) { // webpackBootstrap * for the animation. Default duration is 500 ms. * Only applicable when option focus is true. */ - Timeline.prototype.setSelection = function(ids, options) { + Timeline.prototype.setSelection = function (ids, options) { this.itemSet && this.itemSet.setSelection(ids); if (options && options.focus) { @@ -9951,7 +9817,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the selected items by their id * @return {Array} ids The ids of the selected items */ - Timeline.prototype.getSelection = function() { + Timeline.prototype.getSelection = function () { return this.itemSet && this.itemSet.getSelection() || []; }; @@ -9967,7 +9833,7 @@ return /******/ (function(modules) { // webpackBootstrap * for the animation. Default duration is 500 ms. * Only applicable when option focus is true */ - Timeline.prototype.focus = function(id, options) { + Timeline.prototype.focus = function (id, options) { if (!this.itemsData || id == undefined) return; var ids = Array.isArray(id) ? id : [id]; @@ -9975,8 +9841,8 @@ return /******/ (function(modules) { // webpackBootstrap // get the specified item(s) var itemsData = this.itemsData.getDataSet().get(ids, { type: { - start: 'Date', - end: 'Date' + start: "Date", + end: "Date" } }); @@ -9985,7 +9851,7 @@ return /******/ (function(modules) { // webpackBootstrap var end = null; itemsData.forEach(function (itemData) { var s = itemData.start.valueOf(); - var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); + var e = "end" in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); if (start === null || s < start) { start = s; @@ -9999,9 +9865,9 @@ return /******/ (function(modules) { // webpackBootstrap if (start !== null && end !== null) { // calculate the new middle and interval for the window var middle = (start + end) / 2; - var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1); + var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); - var animate = (options && options.animate !== undefined) ? options.animate : true; + var animate = options && options.animate !== undefined ? options.animate : true; this.range.setRange(middle - interval / 2, middle + interval / 2, animate); } }; @@ -10012,61 +9878,59 @@ return /******/ (function(modules) { // webpackBootstrap * When no minimum is found, min==null * When no maximum is found, max==null */ - Timeline.prototype.getItemRange = function() { + Timeline.prototype.getItemRange = function () { // calculate min from start filed var dataset = this.itemsData.getDataSet(), - min = null, - max = null; + min = null, + max = null; if (dataset) { // calculate the minimum value of the field 'start' - var minItem = dataset.min('start'); - min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; + var minItem = dataset.min("start"); + min = minItem ? util.convert(minItem.start, "Date").valueOf() : null; // Note: we convert first to Date and then to number because else // a conversion from ISODate to Number will fail // calculate maximum value of fields 'start' and 'end' - var maxStartItem = dataset.max('start'); + var maxStartItem = dataset.max("start"); if (maxStartItem) { - max = util.convert(maxStartItem.start, 'Date').valueOf(); + max = util.convert(maxStartItem.start, "Date").valueOf(); } - var maxEndItem = dataset.max('end'); + var maxEndItem = dataset.max("end"); if (maxEndItem) { if (max == null) { - max = util.convert(maxEndItem.end, 'Date').valueOf(); - } - else { - max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); + max = util.convert(maxEndItem.end, "Date").valueOf(); + } else { + max = Math.max(max, util.convert(maxEndItem.end, "Date").valueOf()); } } } return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null }; }; module.exports = Timeline; - /***/ }, /* 19 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // Only load hammer.js when in a browser environment // (loading hammer.js in a node.js environment gives errors) - if (typeof window !== 'undefined') { - module.exports = window['Hammer'] || __webpack_require__(20); - } - else { + if (typeof window !== "undefined") { + module.exports = window.Hammer || __webpack_require__(20); + } else { module.exports = function () { - throw Error('hammer.js is only available in a browser, not in node.js.'); - } + throw Error("hammer.js is only available in a browser, not in node.js."); + }; } - /***/ }, /* 20 */ /***/ function(module, exports, __webpack_require__) { @@ -12238,6 +12102,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 21 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var hammerUtil = __webpack_require__(22); var moment = __webpack_require__(2); @@ -12254,8 +12120,8 @@ return /******/ (function(modules) { // webpackBootstrap */ function Range(body, options) { var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add(-3, 'days').valueOf(); // Number - this.end = now.clone().add(4, 'days').valueOf(); // Number + this.start = now.clone().add(-3, "days").valueOf(); // Number + this.end = now.clone().add(4, "days").valueOf(); // Number this.body = body; this.deltaDifference = 0; @@ -12267,13 +12133,13 @@ return /******/ (function(modules) { // webpackBootstrap this.defaultOptions = { start: null, end: null, - direction: 'horizontal', // 'horizontal' or 'vertical' + direction: "horizontal", // 'horizontal' or 'vertical' moveable: true, zoomable: true, min: null, max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds }; this.options = util.extend({}, this.defaultOptions); @@ -12283,20 +12149,20 @@ return /******/ (function(modules) { // webpackBootstrap this.animateTimer = null; // drag listeners for dragging - this.body.emitter.on('dragstart', this._onDragStart.bind(this)); - this.body.emitter.on('drag', this._onDrag.bind(this)); - this.body.emitter.on('dragend', this._onDragEnd.bind(this)); + this.body.emitter.on("dragstart", this._onDragStart.bind(this)); + this.body.emitter.on("drag", this._onDrag.bind(this)); + this.body.emitter.on("dragend", this._onDragEnd.bind(this)); // ignore dragging when holding - this.body.emitter.on('hold', this._onHold.bind(this)); + this.body.emitter.on("hold", this._onHold.bind(this)); // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); - this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF + this.body.emitter.on("mousewheel", this._onMouseWheel.bind(this)); + this.body.emitter.on("DOMMouseScroll", this._onMouseWheel.bind(this)); // For FF // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); + this.body.emitter.on("touch", this._onTouch.bind(this)); + this.body.emitter.on("pinch", this._onPinch.bind(this)); this.setOptions(options); } @@ -12322,10 +12188,10 @@ return /******/ (function(modules) { // webpackBootstrap Range.prototype.setOptions = function (options) { if (options) { // copy the options that we know - var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates']; + var fields = ["direction", "min", "max", "zoomMin", "zoomMax", "moveable", "zoomable", "activate", "hiddenDates"]; util.selectiveExtend(fields, this.options, options); - if ('start' in options || 'end' in options) { + if ("start" in options || "end" in options) { // apply a new range. both start and end are optional this.setRange(options.start, options.end); } @@ -12336,10 +12202,9 @@ return /******/ (function(modules) { // webpackBootstrap * Test whether direction has a valid value * @param {String} direction 'horizontal' or 'vertical' */ - function validateDirection (direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); + function validateDirection(direction) { + if (direction != "horizontal" && direction != "vertical") { + throw new TypeError("Unknown direction \"" + direction + "\". " + "Choose \"horizontal\" or \"vertical\"."); } } @@ -12355,19 +12220,19 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Boolean} [byUser=false] * */ - Range.prototype.setRange = function(start, end, animate, byUser) { + Range.prototype.setRange = function (start, end, animate, byUser) { if (byUser !== true) { byUser = false; } - var _start = start != undefined ? util.convert(start, 'Date').valueOf() : null; - var _end = end != undefined ? util.convert(end, 'Date').valueOf() : null; + var _start = start != undefined ? util.convert(start, "Date").valueOf() : null; + var _end = end != undefined ? util.convert(end, "Date").valueOf() : null; this._cancelAnimation(); if (animate) { var me = this; var initStart = this.start; var initEnd = this.end; - var duration = typeof animate === 'number' ? animate : 500; + var duration = typeof animate === "number" ? animate : 500; var initTime = new Date().valueOf(); var anyChanged = false; @@ -12376,22 +12241,21 @@ return /******/ (function(modules) { // webpackBootstrap var now = new Date().valueOf(); var time = now - initTime; var done = time > duration; - var s = (done || _start === null) ? _start : util.easeInOutQuad(time, initStart, _start, duration); - var e = (done || _end === null) ? _end : util.easeInOutQuad(time, initEnd, _end, duration); + var s = done || _start === null ? _start : util.easeInOutQuad(time, initStart, _start, duration); + var e = done || _end === null ? _end : util.easeInOutQuad(time, initEnd, _end, duration); changed = me._applyRange(s, e); DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); anyChanged = anyChanged || changed; if (changed) { - me.body.emitter.emit('rangechange', {start: new Date(me.start), end: new Date(me.end), byUser:byUser}); + me.body.emitter.emit("rangechange", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); } if (done) { if (anyChanged) { - me.body.emitter.emit('rangechanged', {start: new Date(me.start), end: new Date(me.end), byUser:byUser}); + me.body.emitter.emit("rangechanged", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); } - } - else { + } else { // animate with as high as possible frame rate, leave 20 ms in between // each to prevent the browser from blocking me.animateTimer = setTimeout(next, 20); @@ -12400,14 +12264,13 @@ return /******/ (function(modules) { // webpackBootstrap }; return next(); - } - else { + } else { var changed = this._applyRange(_start, _end); DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); if (changed) { - var params = {start: new Date(this.start), end: new Date(this.end), byUser:byUser}; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); + var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; + this.body.emitter.emit("rangechange", params); + this.body.emitter.emit("rangechanged", params); } } }; @@ -12432,19 +12295,19 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Boolean} changed * @private */ - Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start, - newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end, - max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, - min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + Range.prototype._applyRange = function (start, end) { + var newStart = start != null ? util.convert(start, "Date").valueOf() : this.start, + newEnd = end != null ? util.convert(end, "Date").valueOf() : this.end, + max = this.options.max != null ? util.convert(this.options.max, "Date").valueOf() : null, + min = this.options.min != null ? util.convert(this.options.min, "Date").valueOf() : null, diff; // check for valid number if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); + throw new Error("Invalid start \"" + start + "\""); } if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); + throw new Error("Invalid end \"" + end + "\""); } // prevent start < end @@ -12455,7 +12318,7 @@ return /******/ (function(modules) { // webpackBootstrap // prevent start < min if (min !== null) { if (newStart < min) { - diff = (min - newStart); + diff = min - newStart; newStart += diff; newEnd += diff; @@ -12471,7 +12334,7 @@ return /******/ (function(modules) { // webpackBootstrap // prevent end > max if (max !== null) { if (newEnd > max) { - diff = (newEnd - max); + diff = newEnd - max; newStart -= diff; newEnd -= diff; @@ -12490,15 +12353,14 @@ return /******/ (function(modules) { // webpackBootstrap if (zoomMin < 0) { zoomMin = 0; } - if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) === zoomMin && newStart > this.start && newEnd < this.end) { + if (newEnd - newStart < zoomMin) { + if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { // ignore this action, we are already zoomed to the minimum newStart = this.start; newEnd = this.end; - } - else { + } else { // zoom to the minimum - diff = (zoomMin - (newEnd - newStart)); + diff = zoomMin - (newEnd - newStart); newStart -= diff / 2; newEnd += diff / 2; } @@ -12512,27 +12374,25 @@ return /******/ (function(modules) { // webpackBootstrap zoomMax = 0; } - if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) === zoomMax && newStart < this.start && newEnd > this.end) { + if (newEnd - newStart > zoomMax) { + if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { // ignore this action, we are already zoomed to the maximum newStart = this.start; newEnd = this.end; - } - else { + } else { // zoom to the maximum - diff = ((newEnd - newStart) - zoomMax); + diff = newEnd - newStart - zoomMax; newStart += diff / 2; newEnd -= diff / 2; } } } - var changed = (this.start != newStart || this.end != newEnd); + var changed = this.start != newStart || this.end != newEnd; // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) - if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) && - !((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) { - this.body.emitter.emit('checkRangedItems'); + if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { + this.body.emitter.emit("checkRangedItems"); } this.start = newStart; @@ -12544,7 +12404,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the current range. * @return {Object} An object with start and end properties */ - Range.prototype.getRange = function() { + Range.prototype.getRange = function () { return { start: this.start, end: this.end @@ -12573,13 +12433,12 @@ return /******/ (function(modules) { // webpackBootstrap if (totalHidden === undefined) { totalHidden = 0; } - if (width != 0 && (end - start != 0)) { + if (width != 0 && end - start != 0) { return { offset: start, scale: width / (end - start - totalHidden) - } - } - else { + }; + } else { return { offset: 0, scale: 1 @@ -12592,7 +12451,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @private */ - Range.prototype._onDragStart = function(event) { + Range.prototype._onDragStart = function (event) { this.deltaDifference = 0; this.previousDelta = 0; // only allow dragging when configured as movable @@ -12607,7 +12466,7 @@ return /******/ (function(modules) { // webpackBootstrap this.props.touch.dragging = true; if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; + this.body.dom.root.style.cursor = "move"; } }; @@ -12626,23 +12485,23 @@ return /******/ (function(modules) { // webpackBootstrap var direction = this.options.direction; validateDirection(direction); - var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY; + var delta = direction == "horizontal" ? event.gesture.deltaX : event.gesture.deltaY; delta -= this.deltaDifference; - var interval = (this.props.touch.end - this.props.touch.start); + var interval = this.props.touch.end - this.props.touch.start; // normalize dragging speed if cutout is in between. var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); interval -= duration; - var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height; + var width = direction == "horizontal" ? this.body.domProps.center.width : this.body.domProps.center.height; var diffRange = -delta / width * interval; var newStart = this.props.touch.start + diffRange; var newEnd = this.props.touch.end + diffRange; // snapping times away from hidden zones - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta-delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta-delta, true); + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); if (safeStart != newStart || safeEnd != newEnd) { this.deltaDifference += delta; this.props.touch.start = safeStart; @@ -12655,9 +12514,9 @@ return /******/ (function(modules) { // webpackBootstrap this._applyRange(newStart, newEnd); // fire a rangechange event - this.body.emitter.emit('rangechange', { + this.body.emitter.emit("rangechange", { start: new Date(this.start), - end: new Date(this.end), + end: new Date(this.end), byUser: true }); }; @@ -12677,13 +12536,13 @@ return /******/ (function(modules) { // webpackBootstrap this.props.touch.dragging = false; if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; + this.body.dom.root.style.cursor = "auto"; } // fire a rangechanged event - this.body.emitter.emit('rangechanged', { + this.body.emitter.emit("rangechanged", { start: new Date(this.start), - end: new Date(this.end), + end: new Date(this.end), byUser: true }); }; @@ -12694,15 +12553,17 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @private */ - Range.prototype._onMouseWheel = function(event) { + Range.prototype._onMouseWheel = function (event) { // only allow zooming when configured as zoomable and moveable if (!(this.options.zoomable && this.options.moveable)) return; // retrieve delta var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ + if (event.wheelDelta) { + /* IE/Opera. */ delta = event.wheelDelta / 120; - } else if (event.detail) { /* Mozilla case. */ + } 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; @@ -12718,10 +12579,9 @@ return /******/ (function(modules) { // webpackBootstrap // equals zooming out with a delta -0.1 var scale; if (delta < 0) { - scale = 1 - (delta / 5); - } - else { - scale = 1 / (1 + (delta / 5)) ; + scale = 1 - delta / 5; + } else { + scale = 1 / (1 + delta / 5); } // calculate center, the date to zoom around @@ -12782,8 +12642,8 @@ return /******/ (function(modules) { // webpackBootstrap var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; // calculate new start and end - var newStart = (centerDate - hiddenDurationBefore) + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; - var newEnd = (centerDate + hiddenDurationAfter) + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; + var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; + var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; // snapping times away from hidden zones this.startToFront = 1 - scale > 0 ? false : true; // used to do the right autocorrection with periodic hidden times @@ -12818,10 +12678,9 @@ return /******/ (function(modules) { // webpackBootstrap validateDirection(direction); - if (direction == 'horizontal') { + if (direction == "horizontal") { return this.body.util.toTime(pointer.x).valueOf(); - } - else { + } else { var height = this.body.domProps.center.height; conversion = this.conversion(height); return pointer.y / conversion.scale + conversion.offset; @@ -12835,7 +12694,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {{x: Number, y: Number}} pointer * @private */ - function getPointer (touch, element) { + function getPointer(touch, element) { return { x: touch.pageX - util.getAbsoluteLeft(element), y: touch.pageY - util.getAbsoluteTop(element) @@ -12852,7 +12711,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} [center] Value representing a date around which will * be zoomed. */ - Range.prototype.zoom = function(scale, center, delta) { + Range.prototype.zoom = function (scale, center, delta) { // if centerDate is not provided, take it half between start Date and end Date if (center == null) { center = (this.start + this.end) / 2; @@ -12863,12 +12722,12 @@ return /******/ (function(modules) { // webpackBootstrap var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; // calculate new start and end - var newStart = (center-hiddenDurationBefore) + (this.start - (center-hiddenDurationBefore)) * scale; - var newEnd = (center+hiddenDurationAfter) + (this.end - (center+hiddenDurationAfter)) * scale; + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; // snapping times away from hidden zones this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); if (safeStart != newStart || safeEnd != newEnd) { @@ -12890,9 +12749,9 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} delta Moving amount. Positive value will move right, * negative value will move left */ - Range.prototype.move = function(delta) { + Range.prototype.move = function (delta) { // zoom start Date and end Date relative to the centerDate - var diff = (this.end - this.start); + var diff = this.end - this.start; // apply new values var newStart = this.start + diff * delta; @@ -12908,7 +12767,7 @@ return /******/ (function(modules) { // webpackBootstrap * Move the range to a new center point * @param {Number} moveTo New center point of the range */ - Range.prototype.moveTo = function(moveTo) { + Range.prototype.moveTo = function (moveTo) { var center = (this.start + this.end) / 2; var diff = center - moveTo; @@ -12922,11 +12781,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Range; - /***/ }, /* 22 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Hammer = __webpack_require__(19); /** @@ -12934,7 +12794,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Element} element * @param {Event} event */ - exports.fakeGesture = function(element, event) { + exports.fakeGesture = function (element, event) { var eventType = null; // for hammer.js 1.0.5 @@ -12956,17 +12816,18 @@ return /******/ (function(modules) { // webpackBootstrap return gesture; }; - /***/ }, /* 23 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * Prototype for visual components * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] * @param {Object} [options] */ - function Component (body, options) { + function Component(body, options) { this.options = null; this.props = null; } @@ -12976,7 +12837,7 @@ return /******/ (function(modules) { // webpackBootstrap * current options. * @param {Object} options */ - Component.prototype.setOptions = function(options) { + Component.prototype.setOptions = function (options) { if (options) { util.extend(this.options, options); } @@ -12986,7 +12847,7 @@ return /******/ (function(modules) { // webpackBootstrap * Repaint the component * @return {boolean} Returns true if the component is resized */ - Component.prototype.redraw = function() { + Component.prototype.redraw = function () { // should be implemented by the component return false; }; @@ -12994,9 +12855,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Destroy the component. Cleanup DOM and event listeners */ - Component.prototype.destroy = function() { - // should be implemented by the component - }; + Component.prototype.destroy = function () {}; /** * Test whether the component is resized since the last time _isResized() was @@ -13004,9 +12863,8 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Boolean} Returns true if the component is resized * @protected */ - Component.prototype._isResized = function() { - var resized = (this.props._previousWidth !== this.props.width || - this.props._previousHeight !== this.props.height); + Component.prototype._isResized = function () { + var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; this.props._previousWidth = this.props.width; this.props._previousHeight = this.props.height; @@ -13015,12 +12873,14 @@ return /******/ (function(modules) { // webpackBootstrap }; module.exports = Component; - + // should be implemented by the component /***/ }, /* 24 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * Created by Alex on 10/3/2014. */ @@ -13032,7 +12892,7 @@ return /******/ (function(modules) { // webpackBootstrap * * @param Core */ - exports.convertHiddenOptions = function(body, hiddenDates) { + exports.convertHiddenOptions = function (body, hiddenDates) { body.hiddenDates = []; if (hiddenDates) { if (Array.isArray(hiddenDates) == true) { @@ -13064,7 +12924,7 @@ return /******/ (function(modules) { // webpackBootstrap var start = moment(body.range.start); var end = moment(body.range.end); - var totalRange = (body.range.end - body.range.start); + var totalRange = body.range.end - body.range.start; var pixelTime = totalRange / body.domProps.centerContainer.width; for (var i = 0; i < hiddenDates.length; i++) { @@ -13081,26 +12941,26 @@ return /******/ (function(modules) { // webpackBootstrap var duration = endDate - startDate; if (duration >= 4 * pixelTime) { - var offset = 0; var runUntil = end.clone(); switch (hiddenDates[i].repeat) { - case "daily": // case of time + case "daily": + // case of time if (startDate.day() != endDate.day()) { offset = 1; } startDate.dayOfYear(start.dayOfYear()); startDate.year(start.year()); - startDate.subtract(7,'days'); + startDate.subtract(7, "days"); endDate.dayOfYear(start.dayOfYear()); endDate.year(start.year()); - endDate.subtract(7 - offset,'days'); + endDate.subtract(7 - offset, "days"); - runUntil.add(1, 'weeks'); + runUntil.add(1, "weeks"); break; case "weekly": - var dayOffset = endDate.diff(startDate,'days') + var dayOffset = endDate.diff(startDate, "days"); var day = startDate.day(); // set the start date to the range.start @@ -13112,69 +12972,69 @@ return /******/ (function(modules) { // webpackBootstrap // force startDate.day(day); endDate.day(day); - endDate.add(dayOffset,'days'); + endDate.add(dayOffset, "days"); - startDate.subtract(1,'weeks'); - endDate.subtract(1,'weeks'); + startDate.subtract(1, "weeks"); + endDate.subtract(1, "weeks"); - runUntil.add(1, 'weeks'); - break + runUntil.add(1, "weeks"); + break; case "monthly": if (startDate.month() != endDate.month()) { offset = 1; } startDate.month(start.month()); startDate.year(start.year()); - startDate.subtract(1,'months'); + startDate.subtract(1, "months"); endDate.month(start.month()); endDate.year(start.year()); - endDate.subtract(1,'months'); - endDate.add(offset,'months'); + endDate.subtract(1, "months"); + endDate.add(offset, "months"); - runUntil.add(1, 'months'); + runUntil.add(1, "months"); break; case "yearly": if (startDate.year() != endDate.year()) { offset = 1; } startDate.year(start.year()); - startDate.subtract(1,'years'); + startDate.subtract(1, "years"); endDate.year(start.year()); - endDate.subtract(1,'years'); - endDate.add(offset,'years'); + endDate.subtract(1, "years"); + endDate.add(offset, "years"); - runUntil.add(1, 'years'); + runUntil.add(1, "years"); break; default: console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); return; } while (startDate < runUntil) { - body.hiddenDates.push({start: startDate.valueOf(), end: endDate.valueOf()}); + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); switch (hiddenDates[i].repeat) { case "daily": - startDate.add(1, 'days'); - endDate.add(1, 'days'); + startDate.add(1, "days"); + endDate.add(1, "days"); break; case "weekly": - startDate.add(1, 'weeks'); - endDate.add(1, 'weeks'); - break + startDate.add(1, "weeks"); + endDate.add(1, "weeks"); + break; case "monthly": - startDate.add(1, 'months'); - endDate.add(1, 'months'); + startDate.add(1, "months"); + endDate.add(1, "months"); break; case "yearly": - startDate.add(1, 'y'); - endDate.add(1, 'y'); + startDate.add(1, "y"); + endDate.add(1, "y"); break; default: console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); return; } } - body.hiddenDates.push({start: startDate.valueOf(), end: endDate.valueOf()}); + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); } } } @@ -13182,17 +13042,20 @@ return /******/ (function(modules) { // webpackBootstrap exports.removeDuplicates(body); // ensure the new positions are not on hidden dates var startHidden = exports.isHidden(body.range.start, body.hiddenDates); - var endHidden = exports.isHidden(body.range.end,body.hiddenDates); + var endHidden = exports.isHidden(body.range.end, body.hiddenDates); var rangeStart = body.range.start; var rangeEnd = body.range.end; - if (startHidden.hidden == true) {rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1;} - if (endHidden.hidden == true) {rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1;} + if (startHidden.hidden == true) { + rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; + } + if (endHidden.hidden == true) { + rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; + } if (startHidden.hidden == true || endHidden.hidden == true) { body.range._applyRange(rangeStart, rangeEnd); } } - - } + }; /** @@ -13200,7 +13063,7 @@ return /******/ (function(modules) { // webpackBootstrap * Scales with N^2 * @param body */ - exports.removeDuplicates = function(body) { + exports.removeDuplicates = function (body) { var hiddenDates = body.hiddenDates; var safeDates = []; for (var i = 0; i < hiddenDates.length; i++) { @@ -13234,20 +13097,20 @@ return /******/ (function(modules) { // webpackBootstrap body.hiddenDates.sort(function (a, b) { return a.start - b.start; }); // sort by start time - } + }; - exports.printDates = function(dates) { - for (var i =0; i < dates.length; i++) { - console.log(i, new Date(dates[i].start),new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); + exports.printDates = function (dates) { + for (var i = 0; i < dates.length; i++) { + console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); } - } + }; /** * Used in TimeStep to avoid the hidden times. * @param timeStep * @param previousTime */ - exports.stepOverHiddenDates = function(timeStep, previousTime) { + exports.stepOverHiddenDates = function (timeStep, previousTime) { var stepInHidden = false; var currentValue = timeStep.current.valueOf(); for (var i = 0; i < timeStep.hiddenDates.length; i++) { @@ -13263,9 +13126,13 @@ return /******/ (function(modules) { // webpackBootstrap var prevValue = moment(previousTime); var newValue = moment(endDate); //check if the next step should be major - if (prevValue.year() != newValue.year()) {timeStep.switchedYear = true;} - else if (prevValue.month() != newValue.month()) {timeStep.switchedMonth = true;} - else if (prevValue.dayOfYear() != newValue.dayOfYear()) {timeStep.switchedDay = true;} + if (prevValue.year() != newValue.year()) { + timeStep.switchedYear = true; + } else if (prevValue.month() != newValue.month()) { + timeStep.switchedMonth = true; + } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { + timeStep.switchedDay = true; + } timeStep.current = newValue.toDate(); } @@ -13302,13 +13169,12 @@ return /******/ (function(modules) { // webpackBootstrap * @param width * @returns {number} */ - exports.toScreen = function(Core, time, width) { + exports.toScreen = function (Core, time, width) { if (Core.body.hiddenDates.length == 0) { var conversion = Core.range.conversion(width); return (time.valueOf() - conversion.offset) * conversion.scale; - } - else { - var hidden = exports.isHidden(time, Core.body.hiddenDates) + } else { + var hidden = exports.isHidden(time, Core.body.hiddenDates); if (hidden.hidden == true) { time = hidden.startDate; } @@ -13330,12 +13196,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param width * @returns {Date} */ - exports.toTime = function(Core, x, width) { + exports.toTime = function (Core, x, width) { if (Core.body.hiddenDates.length == 0) { var conversion = Core.range.conversion(width); return new Date(x / conversion.scale + conversion.offset); - } - else { + } else { var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); var totalDuration = Core.range.end - Core.range.start - hiddenDuration; var partialDuration = totalDuration * x / width; @@ -13354,7 +13219,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param range * @returns {number} */ - exports.getHiddenDurationBetween = function(hiddenDates, start, end) { + exports.getHiddenDurationBetween = function (hiddenDates, start, end) { var duration = 0; for (var i = 0; i < hiddenDates.length; i++) { var startDate = hiddenDates[i].start; @@ -13375,13 +13240,13 @@ return /******/ (function(modules) { // webpackBootstrap * @param time * @returns {{duration: number, time: *, offset: number}} */ - exports.correctTimeForHidden = function(hiddenDates, range, time) { + exports.correctTimeForHidden = function (hiddenDates, range, time) { time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates,range,time); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); return time; }; - exports.getHiddenDurationBefore = function(hiddenDates, range, time) { + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { var timeOffset = 0; time = moment(time).toDate().valueOf(); @@ -13391,12 +13256,12 @@ return /******/ (function(modules) { // webpackBootstrap // if time after the cutout, and the if (startDate >= range.start && endDate < range.end) { if (time >= endDate) { - timeOffset += (endDate - startDate); + timeOffset += endDate - startDate; } } } return timeOffset; - } + }; /** * sum the duration from start to finish, including the hidden duration, @@ -13406,7 +13271,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param time * @returns {{duration: number, time: *, offset: number}} */ - exports.getAccumulatedHiddenDuration = function(hiddenDates, range, requiredDuration) { + exports.getAccumulatedHiddenDuration = function (hiddenDates, range, requiredDuration) { var hiddenDuration = 0; var duration = 0; var previousPoint = range.start; @@ -13420,8 +13285,7 @@ return /******/ (function(modules) { // webpackBootstrap previousPoint = endDate; if (duration >= requiredDuration) { break; - } - else { + } else { hiddenDuration += endDate - startDate; } } @@ -13440,31 +13304,26 @@ return /******/ (function(modules) { // webpackBootstrap * @param correctionEnabled * @returns {*} */ - exports.snapAwayFromHidden = function(hiddenDates, time, direction, correctionEnabled) { + exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { var isHidden = exports.isHidden(time, hiddenDates); if (isHidden.hidden == true) { if (direction < 0) { if (correctionEnabled == true) { return isHidden.startDate - (isHidden.endDate - time) - 1; - } - else { + } else { return isHidden.startDate - 1; } - } - else { + } else { if (correctionEnabled == true) { return isHidden.endDate + (time - isHidden.startDate) + 1; - } - else { + } else { return isHidden.endDate + 1; } } - } - else { + } else { return time; } - - } + }; /** @@ -13474,23 +13333,26 @@ return /******/ (function(modules) { // webpackBootstrap * @param hiddenDates * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ - exports.isHidden = function(time, hiddenDates) { + exports.isHidden = function (time, hiddenDates) { for (var i = 0; i < hiddenDates.length; i++) { var startDate = hiddenDates[i].start; var endDate = hiddenDates[i].end; - if (time >= startDate && time < endDate) { // if the start is entering a hidden zone - return {hidden: true, startDate: startDate, endDate: endDate}; + if (time >= startDate && time < endDate) { + // if the start is entering a hidden zone + return { hidden: true, startDate: startDate, endDate: endDate }; break; } } - return {hidden: false, startDate: startDate, endDate: endDate}; - } + return { hidden: false, startDate: startDate, endDate: endDate }; + }; /***/ }, /* 25 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Emitter = __webpack_require__(11); var Hammer = __webpack_require__(19); var util = __webpack_require__(1); @@ -13509,7 +13371,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [options] See Core.setOptions for the available options. * @constructor */ - function Core () {} + function Core() {} // turn Core into an event emitter Emitter(Core.prototype); @@ -13524,43 +13386,43 @@ return /******/ (function(modules) { // webpackBootstrap Core.prototype._create = function (container) { this.dom = {}; - this.dom.root = document.createElement('div'); - this.dom.background = document.createElement('div'); - this.dom.backgroundVertical = document.createElement('div'); - this.dom.backgroundHorizontal = document.createElement('div'); - this.dom.centerContainer = document.createElement('div'); - this.dom.leftContainer = document.createElement('div'); - this.dom.rightContainer = document.createElement('div'); - this.dom.center = document.createElement('div'); - this.dom.left = document.createElement('div'); - this.dom.right = document.createElement('div'); - this.dom.top = document.createElement('div'); - this.dom.bottom = document.createElement('div'); - this.dom.shadowTop = document.createElement('div'); - this.dom.shadowBottom = document.createElement('div'); - this.dom.shadowTopLeft = document.createElement('div'); - this.dom.shadowBottomLeft = document.createElement('div'); - this.dom.shadowTopRight = document.createElement('div'); - this.dom.shadowBottomRight = document.createElement('div'); - - this.dom.root.className = 'vis timeline root'; - this.dom.background.className = 'vispanel background'; - this.dom.backgroundVertical.className = 'vispanel background vertical'; - this.dom.backgroundHorizontal.className = 'vispanel background horizontal'; - this.dom.centerContainer.className = 'vispanel center'; - this.dom.leftContainer.className = 'vispanel left'; - this.dom.rightContainer.className = 'vispanel right'; - this.dom.top.className = 'vispanel top'; - this.dom.bottom.className = 'vispanel bottom'; - this.dom.left.className = 'content'; - this.dom.center.className = 'content'; - this.dom.right.className = 'content'; - this.dom.shadowTop.className = 'shadow top'; - this.dom.shadowBottom.className = 'shadow bottom'; - this.dom.shadowTopLeft.className = 'shadow top'; - this.dom.shadowBottomLeft.className = 'shadow bottom'; - this.dom.shadowTopRight.className = 'shadow top'; - this.dom.shadowBottomRight.className = 'shadow bottom'; + this.dom.root = document.createElement("div"); + this.dom.background = document.createElement("div"); + this.dom.backgroundVertical = document.createElement("div"); + this.dom.backgroundHorizontal = document.createElement("div"); + this.dom.centerContainer = document.createElement("div"); + this.dom.leftContainer = document.createElement("div"); + this.dom.rightContainer = document.createElement("div"); + this.dom.center = document.createElement("div"); + this.dom.left = document.createElement("div"); + this.dom.right = document.createElement("div"); + this.dom.top = document.createElement("div"); + this.dom.bottom = document.createElement("div"); + this.dom.shadowTop = document.createElement("div"); + this.dom.shadowBottom = document.createElement("div"); + this.dom.shadowTopLeft = document.createElement("div"); + this.dom.shadowBottomLeft = document.createElement("div"); + this.dom.shadowTopRight = document.createElement("div"); + this.dom.shadowBottomRight = document.createElement("div"); + + this.dom.root.className = "vis timeline root"; + this.dom.background.className = "vispanel background"; + this.dom.backgroundVertical.className = "vispanel background vertical"; + this.dom.backgroundHorizontal.className = "vispanel background horizontal"; + this.dom.centerContainer.className = "vispanel center"; + this.dom.leftContainer.className = "vispanel left"; + this.dom.rightContainer.className = "vispanel right"; + this.dom.top.className = "vispanel top"; + this.dom.bottom.className = "vispanel bottom"; + this.dom.left.className = "content"; + this.dom.center.className = "content"; + this.dom.right.className = "content"; + this.dom.shadowTop.className = "shadow top"; + this.dom.shadowBottom.className = "shadow bottom"; + this.dom.shadowTopLeft.className = "shadow top"; + this.dom.shadowBottomLeft.className = "shadow bottom"; + this.dom.shadowTopRight.className = "shadow top"; + this.dom.shadowBottomRight.className = "shadow bottom"; this.dom.root.appendChild(this.dom.background); this.dom.root.appendChild(this.dom.backgroundVertical); @@ -13582,24 +13444,23 @@ return /******/ (function(modules) { // webpackBootstrap this.dom.rightContainer.appendChild(this.dom.shadowTopRight); this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - this.on('rangechange', this._redraw.bind(this)); - this.on('touch', this._onTouch.bind(this)); - this.on('pinch', this._onPinch.bind(this)); - this.on('dragstart', this._onDragStart.bind(this)); - this.on('drag', this._onDrag.bind(this)); + this.on("rangechange", this._redraw.bind(this)); + this.on("touch", this._onTouch.bind(this)); + this.on("pinch", this._onPinch.bind(this)); + this.on("dragstart", this._onDragStart.bind(this)); + this.on("drag", this._onDrag.bind(this)); var me = this; - this.on('change', function (properties) { + this.on("change", function (properties) { if (properties && properties.queue == true) { // redraw once on next tick if (!me._redrawTimer) { me._redrawTimer = setTimeout(function () { me._redrawTimer = null; me._redraw(); - }, 0) + }, 0); } - } - else { + } else { // redraw immediately me._redraw(); } @@ -13612,11 +13473,7 @@ return /******/ (function(modules) { // webpackBootstrap }); this.listeners = {}; - var events = [ - 'touch', 'pinch', - 'tap', 'doubletap', 'hold', - 'dragstart', 'drag', 'dragend', - 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + var events = ["touch", "pinch", "tap", "doubletap", "hold", "dragstart", "drag", "dragend", "mousewheel", "DOMMouseScroll" // DOMMouseScroll is needed for Firefox ]; events.forEach(function (event) { var listener = function () { @@ -13650,7 +13507,7 @@ return /******/ (function(modules) { // webpackBootstrap this.redrawCount = 0; // attach the root panel to the provided container - if (!container) throw new Error('No container provided'); + if (!container) throw new Error("No container provided"); container.appendChild(this.dom.root); }; @@ -13682,20 +13539,19 @@ return /******/ (function(modules) { // webpackBootstrap Core.prototype.setOptions = function (options) { if (options) { // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse', 'dataAttributes', 'hiddenDates']; + var fields = ["width", "height", "minHeight", "maxHeight", "autoResize", "start", "end", "orientation", "clickToUse", "dataAttributes", "hiddenDates"]; util.selectiveExtend(fields, this.options, options); - if ('hiddenDates' in this.options) { + if ("hiddenDates" in this.options) { DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); } - if ('clickToUse' in options) { + if ("clickToUse" in options) { if (options.clickToUse) { if (!this.activator) { this.activator = new Activator(this.dom.root); } - } - else { + } else { if (this.activator) { this.activator.destroy(); delete this.activator; @@ -13709,12 +13565,12 @@ return /******/ (function(modules) { // webpackBootstrap // propagate options to all components this.components.forEach(function (component) { - component.setOptions(options); + return component.setOptions(options); }); // TODO: remove deprecation error one day (deprecated since version 0.8.0) if (options && options.order) { - throw new Error('Option order is deprecated. There is no replacement for this feature.'); + throw new Error("Option order is deprecated. There is no replacement for this feature."); } // redraw everything @@ -13765,7 +13621,7 @@ return /******/ (function(modules) { // webpackBootstrap // give all components the opportunity to cleanup this.components.forEach(function (component) { - component.destroy(); + return component.destroy(); }); this.body = null; @@ -13779,7 +13635,7 @@ return /******/ (function(modules) { // webpackBootstrap */ Core.prototype.setCustomTime = function (time, id) { if (!this.customTime) { - throw new Error('Cannot get custom time: Custom time bar is not enabled'); + throw new Error("Cannot get custom time: Custom time bar is not enabled"); } var barId = id || 0; @@ -13796,9 +13652,9 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Date} customTime * @param {int} id */ - Core.prototype.getCustomTime = function(id) { + Core.prototype.getCustomTime = function (id) { if (!this.customTime) { - throw new Error('Cannot get custom time: Custom time bar is not enabled'); + throw new Error("Cannot get custom time: Custom time bar is not enabled"); } var barId = id || 0, @@ -13822,15 +13678,17 @@ return /******/ (function(modules) { // webpackBootstrap */ Core.prototype.addCustomTime = function (time, id) { if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); + throw new Error("Option showCurrentTime must be true"); } if (time === undefined) { - throw new Error('Time parameter for the custom bar must be provided'); + throw new Error("Time parameter for the custom bar must be provided"); } - var ts = util.convert(time, 'Date').valueOf(), - numIds, customTime, customBarId; + var ts = util.convert(time, "Date").valueOf(), + numIds, + customTime, + customBarId; // All bar IDs are kept in 1 array, mixed types // Bar with ID 0 is the default bar. @@ -13840,19 +13698,16 @@ return /******/ (function(modules) { // webpackBootstrap // If the ID is not provided, generate one, otherwise just use it if (id === undefined) { - numIds = this.customBarIds.filter(function (element) { return util.isNumber(element); }); customBarId = numIds.length > 0 ? Math.max.apply(null, numIds) + 1 : 1; - } else { - // Check for duplicates this.customBarIds.forEach(function (element) { if (element === id) { - throw new Error('Custom time ID already exists'); + throw new Error("Custom time ID already exists"); } }); @@ -13862,9 +13717,9 @@ return /******/ (function(modules) { // webpackBootstrap this.customBarIds.push(customBarId); customTime = new CustomTime(this.body, { - showCustomTime : true, - time : ts, - id : customBarId + showCustomTime: true, + time: ts, + id: customBarId }); this.components.push(customTime); @@ -13879,7 +13734,6 @@ return /******/ (function(modules) { // webpackBootstrap * @return {boolean} True if the bar exists and is removed, false otherwise */ Core.prototype.removeCustomTime = function (id) { - var me = this; this.components.forEach(function (bar, index, components) { @@ -13899,7 +13753,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the id's of the currently visible items. * @returns {Array} The ids of the visible items */ - Core.prototype.getVisibleItems = function() { + Core.prototype.getVisibleItems = function () { return this.itemSet && this.itemSet.getVisibleItems() || []; }; @@ -13915,7 +13769,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [what] Optionally specify what to clear. By default: * {items: true, groups: true, options: true} */ - Core.prototype.clear = function(what) { + Core.prototype.clear = function (what) { // clear items if (!what || what.items) { this.setItems(null); @@ -13929,7 +13783,7 @@ return /******/ (function(modules) { // webpackBootstrap // clear options of timeline and of each of the components if (!what || what.options) { this.components.forEach(function (component) { - component.setOptions(component.defaultOptions); + return component.setOptions(component.defaultOptions); }); this.setOptions(this.defaultOptions); // this will also do a redraw @@ -13945,7 +13799,7 @@ return /******/ (function(modules) { // webpackBootstrap * If a number, the number is taken as duration * for the animation. Default duration is 500 ms. */ - Core.prototype.fit = function(options) { + Core.prototype.fit = function (options) { var range = this._getDataRange(); // skip range set if there is no start and end date @@ -13953,7 +13807,7 @@ return /******/ (function(modules) { // webpackBootstrap return; } - var animate = (options && options.animate !== undefined) ? options.animate : true; + var animate = options && options.animate !== undefined ? options.animate : true; this.range.setRange(range.start, range.end, animate); }; @@ -13962,7 +13816,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {{start: Date | null, end: Date | null}} * @protected */ - Core.prototype._getDataRange = function() { + Core.prototype._getDataRange = function () { // apply the data range as range var dataRange = this.getItemRange(); @@ -13970,7 +13824,7 @@ return /******/ (function(modules) { // webpackBootstrap var start = dataRange.min; var end = dataRange.max; if (start != null && end != null) { - var interval = (end.valueOf() - start.valueOf()); + var interval = end.valueOf() - start.valueOf(); if (interval <= 0) { // prevent an empty interval interval = 24 * 60 * 60 * 1000; // 1 day @@ -13982,7 +13836,7 @@ return /******/ (function(modules) { // webpackBootstrap return { start: start, end: end - } + }; }; /** @@ -14005,15 +13859,14 @@ return /******/ (function(modules) { // webpackBootstrap * If a number, the number is taken as duration * for the animation. Default duration is 500 ms. */ - Core.prototype.setWindow = function(start, end, options) { + Core.prototype.setWindow = function (start, end, options) { var animate; if (arguments.length == 1) { var range = arguments[0]; - animate = (range.animate !== undefined) ? range.animate : true; + animate = range.animate !== undefined ? range.animate : true; this.range.setRange(range.start, range.end, animate); - } - else { - animate = (options && options.animate !== undefined) ? options.animate : true; + } else { + animate = options && options.animate !== undefined ? options.animate : true; this.range.setRange(start, end, animate); } }; @@ -14028,13 +13881,13 @@ return /******/ (function(modules) { // webpackBootstrap * If a number, the number is taken as duration * for the animation. Default duration is 500 ms. */ - Core.prototype.moveTo = function(time, options) { + Core.prototype.moveTo = function (time, options) { var interval = this.range.end - this.range.start; - var t = util.convert(time, 'Date').valueOf(); + var t = util.convert(time, "Date").valueOf(); var start = t - interval / 2; var end = t + interval / 2; - var animate = (options && options.animate !== undefined) ? options.animate : true; + var animate = options && options.animate !== undefined ? options.animate : true; this.range.setRange(start, end, animate); }; @@ -14043,7 +13896,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the visible window * @return {{start: Date, end: Date}} Visible range */ - Core.prototype.getWindow = function() { + Core.prototype.getWindow = function () { var range = this.range.getRange(); return { start: new Date(range.start), @@ -14054,7 +13907,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Force a redraw. Can be overridden by implementations of Core */ - Core.prototype.redraw = function() { + Core.prototype.redraw = function () { this._redraw(); }; @@ -14063,7 +13916,7 @@ return /******/ (function(modules) { // webpackBootstrap * method redraw. * @protected */ - Core.prototype._redraw = function() { + Core.prototype._redraw = function () { var resized = false; var options = this.options; var props = this.props; @@ -14074,33 +13927,32 @@ return /******/ (function(modules) { // webpackBootstrap DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); // update class names - if (options.orientation == 'top') { - util.addClassName(dom.root, 'top'); - util.removeClassName(dom.root, 'bottom'); - } - else { - util.removeClassName(dom.root, 'top'); - util.addClassName(dom.root, 'bottom'); + if (options.orientation == "top") { + util.addClassName(dom.root, "top"); + util.removeClassName(dom.root, "bottom"); + } else { + util.removeClassName(dom.root, "top"); + util.addClassName(dom.root, "bottom"); } // update root width and height options - dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); - dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); - dom.root.style.width = util.option.asSize(options.width, ''); + dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ""); + dom.root.style.minHeight = util.option.asSize(options.minHeight, ""); + dom.root.style.width = util.option.asSize(options.width, ""); // calculate border widths - props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; - props.border.right = props.border.left; - props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; + props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; + props.border.right = props.border.left; + props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; props.border.bottom = props.border.top; - var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight; + var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; // workaround for a bug in IE: the clientWidth of an element with // a height:0px and overflow:hidden is not calculated and always has value 0 if (dom.centerContainer.clientHeight === 0) { props.border.left = props.border.top; - props.border.right = props.border.left; + props.border.right = props.border.left; } if (dom.root.clientHeight === 0) { borderRootWidth = borderRootHeight; @@ -14109,9 +13961,9 @@ return /******/ (function(modules) { // webpackBootstrap // calculate the heights. If any of the side panels is empty, we set the height to // minus the border width, such that the border will be invisible props.center.height = dom.center.offsetHeight; - props.left.height = dom.left.offsetHeight; - props.right.height = dom.right.offsetHeight; - props.top.height = dom.top.clientHeight || -props.border.top; + props.left.height = dom.left.offsetHeight; + props.right.height = dom.right.offsetHeight; + props.top.height = dom.top.clientHeight || -props.border.top; props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; // TODO: compensate borders when any of the panels is empty. @@ -14119,64 +13971,62 @@ return /******/ (function(modules) { // webpackBootstrap // apply auto height // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); - var autoHeight = props.top.height + contentHeight + props.bottom.height + - borderRootHeight + props.border.top + props.border.bottom; - dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; + dom.root.style.height = util.option.asSize(options.height, autoHeight + "px"); // calculate heights of the content panels props.root.height = dom.root.offsetHeight; props.background.height = props.root.height - borderRootHeight; - var containerHeight = props.root.height - props.top.height - props.bottom.height - - borderRootHeight; - props.centerContainer.height = containerHeight; - props.leftContainer.height = containerHeight; - props.rightContainer.height = props.leftContainer.height; + var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; + props.centerContainer.height = containerHeight; + props.leftContainer.height = containerHeight; + props.rightContainer.height = props.leftContainer.height; // calculate the widths of the panels props.root.width = dom.root.offsetWidth; props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.left.width = dom.leftContainer.clientWidth || -props.border.left; props.leftContainer.width = props.left.width; props.right.width = dom.rightContainer.clientWidth || -props.border.right; props.rightContainer.width = props.right.width; var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; - props.center.width = centerWidth; + props.center.width = centerWidth; props.centerContainer.width = centerWidth; - props.top.width = centerWidth; - props.bottom.width = centerWidth; + props.top.width = centerWidth; + props.bottom.width = centerWidth; // resize the panels - dom.background.style.height = props.background.height + 'px'; - dom.backgroundVertical.style.height = props.background.height + 'px'; - dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; - dom.centerContainer.style.height = props.centerContainer.height + 'px'; - dom.leftContainer.style.height = props.leftContainer.height + 'px'; - dom.rightContainer.style.height = props.rightContainer.height + 'px'; - - dom.background.style.width = props.background.width + 'px'; - dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; - dom.backgroundHorizontal.style.width = props.background.width + 'px'; - dom.centerContainer.style.width = props.center.width + 'px'; - dom.top.style.width = props.top.width + 'px'; - dom.bottom.style.width = props.bottom.width + 'px'; + dom.background.style.height = props.background.height + "px"; + dom.backgroundVertical.style.height = props.background.height + "px"; + dom.backgroundHorizontal.style.height = props.centerContainer.height + "px"; + dom.centerContainer.style.height = props.centerContainer.height + "px"; + dom.leftContainer.style.height = props.leftContainer.height + "px"; + dom.rightContainer.style.height = props.rightContainer.height + "px"; + + dom.background.style.width = props.background.width + "px"; + dom.backgroundVertical.style.width = props.centerContainer.width + "px"; + dom.backgroundHorizontal.style.width = props.background.width + "px"; + dom.centerContainer.style.width = props.center.width + "px"; + dom.top.style.width = props.top.width + "px"; + dom.bottom.style.width = props.bottom.width + "px"; // reposition the panels - dom.background.style.left = '0'; - dom.background.style.top = '0'; - dom.backgroundVertical.style.left = (props.left.width + props.border.left) + 'px'; - dom.backgroundVertical.style.top = '0'; - dom.backgroundHorizontal.style.left = '0'; - dom.backgroundHorizontal.style.top = props.top.height + 'px'; - dom.centerContainer.style.left = props.left.width + 'px'; - dom.centerContainer.style.top = props.top.height + 'px'; - dom.leftContainer.style.left = '0'; - dom.leftContainer.style.top = props.top.height + 'px'; - dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px'; - dom.rightContainer.style.top = props.top.height + 'px'; - dom.top.style.left = props.left.width + 'px'; - dom.top.style.top = '0'; - dom.bottom.style.left = props.left.width + 'px'; - dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px'; + dom.background.style.left = "0"; + dom.background.style.top = "0"; + dom.backgroundVertical.style.left = props.left.width + props.border.left + "px"; + dom.backgroundVertical.style.top = "0"; + dom.backgroundHorizontal.style.left = "0"; + dom.backgroundHorizontal.style.top = props.top.height + "px"; + dom.centerContainer.style.left = props.left.width + "px"; + dom.centerContainer.style.top = props.top.height + "px"; + dom.leftContainer.style.left = "0"; + dom.leftContainer.style.top = props.top.height + "px"; + dom.rightContainer.style.left = props.left.width + props.center.width + "px"; + dom.rightContainer.style.top = props.top.height + "px"; + dom.top.style.left = props.left.width + "px"; + dom.top.style.top = "0"; + dom.bottom.style.left = props.left.width + "px"; + dom.bottom.style.top = props.top.height + props.centerContainer.height + "px"; // update the scrollTop, feasible range for the offset can be changed // when the height of the Core or of the contents of the center changed @@ -14184,26 +14034,25 @@ return /******/ (function(modules) { // webpackBootstrap // reposition the scrollable contents var offset = this.props.scrollTop; - if (options.orientation == 'bottom') { - offset += Math.max(this.props.centerContainer.height - this.props.center.height - - this.props.border.top - this.props.border.bottom, 0); + if (options.orientation == "bottom") { + offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); } - dom.center.style.left = '0'; - dom.center.style.top = offset + 'px'; - dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; - dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; + dom.center.style.left = "0"; + dom.center.style.top = offset + "px"; + dom.left.style.left = "0"; + dom.left.style.top = offset + "px"; + dom.right.style.left = "0"; + dom.right.style.top = offset + "px"; // show shadows when vertical scrolling is available - var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; - var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; - dom.shadowTop.style.visibility = visibilityTop; - dom.shadowBottom.style.visibility = visibilityBottom; - dom.shadowTopLeft.style.visibility = visibilityTop; - dom.shadowBottomLeft.style.visibility = visibilityBottom; - dom.shadowTopRight.style.visibility = visibilityTop; - dom.shadowBottomRight.style.visibility = visibilityBottom; + var visibilityTop = this.props.scrollTop == 0 ? "hidden" : ""; + var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? "hidden" : ""; + dom.shadowTop.style.visibility = visibilityTop; + dom.shadowBottom.style.visibility = visibilityBottom; + dom.shadowTopLeft.style.visibility = visibilityTop; + dom.shadowBottomLeft.style.visibility = visibilityBottom; + dom.shadowTopRight.style.visibility = visibilityTop; + dom.shadowBottomRight.style.visibility = visibilityBottom; // redraw all components this.components.forEach(function (component) { @@ -14215,9 +14064,8 @@ return /******/ (function(modules) { // webpackBootstrap if (this.redrawCount < MAX_REDRAWS) { this.redrawCount++; this._redraw(); - } - else { - console.log('WARNING: infinite loop in redraw?'); + } else { + console.log("WARNING: infinite loop in redraw?"); } this.redrawCount = 0; } @@ -14227,7 +14075,7 @@ return /******/ (function(modules) { // webpackBootstrap // TODO: deprecated since version 1.1.0, remove some day Core.prototype.repaint = function () { - throw new Error('Function repaint is deprecated. Use redraw instead.'); + throw new Error("Function repaint is deprecated. Use redraw instead."); }; /** @@ -14237,9 +14085,9 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Date | String | Number} time A Date, unix timestamp, or * ISO date string. */ - Core.prototype.setCurrentTime = function(time) { + Core.prototype.setCurrentTime = function (time) { if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); + throw new Error("Option showCurrentTime must be true"); } this.currentTime.setCurrentTime(time); @@ -14250,9 +14098,9 @@ return /******/ (function(modules) { // webpackBootstrap * Only applicable when option `showCurrentTime` is true. * @return {Date} Returns the current time. */ - Core.prototype.getCurrentTime = function() { + Core.prototype.getCurrentTime = function () { if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); + throw new Error("Option showCurrentTime must be true"); } return this.currentTime.getCurrentTime(); @@ -14265,7 +14113,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ // TODO: move this function to Range - Core.prototype._toTime = function(x) { + Core.prototype._toTime = function (x) { return DateUtil.toTime(this, x, this.props.center.width); }; @@ -14276,7 +14124,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ // TODO: move this function to Range - Core.prototype._toGlobalTime = function(x) { + Core.prototype._toGlobalTime = function (x) { return DateUtil.toTime(this, x, this.props.root.width); //var conversion = this.range.conversion(this.props.root.width); //return new Date(x / conversion.scale + conversion.offset); @@ -14290,7 +14138,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ // TODO: move this function to Range - Core.prototype._toScreen = function(time) { + Core.prototype._toScreen = function (time) { return DateUtil.toScreen(this, time, this.props.center.width); }; @@ -14305,7 +14153,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ // TODO: move this function to Range - Core.prototype._toGlobalScreen = function(time) { + Core.prototype._toGlobalScreen = function (time) { return DateUtil.toScreen(this, time, this.props.root.width); //var conversion = this.range.conversion(this.props.root.width); //return (time.valueOf() - conversion.offset) * conversion.scale; @@ -14319,8 +14167,7 @@ return /******/ (function(modules) { // webpackBootstrap Core.prototype._initAutoResize = function () { if (this.options.autoResize == true) { this._startAutoResize(); - } - else { + } else { this._stopAutoResize(); } }; @@ -14335,7 +14182,7 @@ return /******/ (function(modules) { // webpackBootstrap this._stopAutoResize(); - this._onResize = function() { + this._onResize = function () { if (me.options.autoResize != true) { // stop watching when the option autoResize is changed to false me._stopAutoResize(); @@ -14347,18 +14194,17 @@ return /******/ (function(modules) { // webpackBootstrap // Note: we compare offsetWidth here, not clientWidth. For some reason, // IE does not restore the clientWidth from 0 to the actual width after // changing the timeline's container display style from none to visible - if ((me.dom.root.offsetWidth != me.props.lastWidth) || - (me.dom.root.offsetHeight != me.props.lastHeight)) { + if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { me.props.lastWidth = me.dom.root.offsetWidth; me.props.lastHeight = me.dom.root.offsetHeight; - me.emit('change'); + me.emit("change"); } } }; // add event listener to window resize - util.addEventListener(window, 'resize', this._onResize); + util.addEventListener(window, "resize", this._onResize); this.watchTimer = setInterval(this._onResize, 1000); }; @@ -14374,7 +14220,7 @@ return /******/ (function(modules) { // webpackBootstrap } // remove event listener on window.resize - util.removeEventListener(window, 'resize', this._onResize); + util.removeEventListener(window, "resize", this._onResize); this._onResize = null; }; @@ -14450,8 +14296,8 @@ return /******/ (function(modules) { // webpackBootstrap if (scrollTopMin != this.props.scrollTopMin) { // in case of bottom orientation, change the scrollTop such that the contents // do not move relative to the time axis at the bottom - if (this.options.orientation == 'bottom') { - this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin); + if (this.options.orientation == "bottom") { + this.props.scrollTop += scrollTopMin - this.props.scrollTopMin; } this.props.scrollTopMin = scrollTopMin; } @@ -14474,11 +14320,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Core; - /***/ }, /* 26 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Hammer = __webpack_require__(19); var util = __webpack_require__(1); var DataSet = __webpack_require__(7); @@ -14493,8 +14340,8 @@ return /******/ (function(modules) { // webpackBootstrap var BackgroundItem = __webpack_require__(35); - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items - var BACKGROUND = '__background__'; // reserved group id for background items without group + var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items + var BACKGROUND = "__background__"; // reserved group id for background items without group /** * An ItemSet holds a set of items and ranges which can be displayed in a @@ -14509,9 +14356,9 @@ return /******/ (function(modules) { // webpackBootstrap this.body = body; this.defaultOptions = { - type: null, // 'box', 'point', 'range', 'background' - orientation: 'bottom', // 'top' or 'bottom' - align: 'auto', // alignment of box items + type: null, // 'box', 'point', 'range', 'background' + orientation: "bottom", // 'top' or 'bottom' + align: "auto", // alignment of box items stack: true, groupOrder: null, @@ -14523,7 +14370,7 @@ return /******/ (function(modules) { // webpackBootstrap remove: false }, - snap: TimeStep.snap, + snap: TimeStep.snap, onAdd: function (item, callback) { callback(item); @@ -14556,7 +14403,7 @@ return /******/ (function(modules) { // webpackBootstrap // options for getting items from the DataSet with the correct type this.itemOptions = { - type: {start: 'Date', end: 'Date'} + type: { start: "Date", end: "Date" } }; this.conversion = { @@ -14568,40 +14415,40 @@ return /******/ (function(modules) { // webpackBootstrap this.hammer = null; var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet // listeners for the DataSet of the items this.itemListeners = { - 'add': function (event, params, senderId) { + add: function (event, params, senderId) { me._onAdd(params.items); }, - 'update': function (event, params, senderId) { + update: function (event, params, senderId) { me._onUpdate(params.items); }, - 'remove': function (event, params, senderId) { + remove: function (event, params, senderId) { me._onRemove(params.items); } }; // listeners for the DataSet of the groups this.groupListeners = { - 'add': function (event, params, senderId) { + add: function (event, params, senderId) { me._onAddGroups(params.items); }, - 'update': function (event, params, senderId) { + update: function (event, params, senderId) { me._onUpdateGroups(params.items); }, - 'remove': function (event, params, senderId) { + remove: function (event, params, senderId) { me._onRemoveGroups(params.items); } }; - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group this.groupIds = []; - this.selection = []; // list with the ids of all selected nodes + this.selection = []; // list with the ids of all selected nodes this.stackDirty = true; // if true, all items will be restacked on next redraw this.touchParams = {}; // stores properties while dragging @@ -14625,32 +14472,32 @@ return /******/ (function(modules) { // webpackBootstrap /** * Create the HTML DOM for the ItemSet */ - ItemSet.prototype._create = function(){ - var frame = document.createElement('div'); - frame.className = 'itemset'; - frame['timeline-itemset'] = this; + ItemSet.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "itemset"; + frame["timeline-itemset"] = this; this.dom.frame = frame; // create background panel - var background = document.createElement('div'); - background.className = 'background'; + var background = document.createElement("div"); + background.className = "background"; frame.appendChild(background); this.dom.background = background; // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'foreground'; + var foreground = document.createElement("div"); + foreground.className = "foreground"; frame.appendChild(foreground); this.dom.foreground = foreground; // create axis panel - var axis = document.createElement('div'); - axis.className = 'axis'; + var axis = document.createElement("div"); + axis.className = "axis"; this.dom.axis = axis; // create labelset - var labelSet = document.createElement('div'); - labelSet.className = 'labelset'; + var labelSet = document.createElement("div"); + labelSet.className = "labelset"; this.dom.labelSet = labelSet; // create ungrouped Group @@ -14670,19 +14517,19 @@ return /******/ (function(modules) { // webpackBootstrap }); // drag items when selected - this.hammer.on('touch', this._onTouch.bind(this)); - this.hammer.on('dragstart', this._onDragStart.bind(this)); - this.hammer.on('drag', this._onDrag.bind(this)); - this.hammer.on('dragend', this._onDragEnd.bind(this)); + this.hammer.on("touch", this._onTouch.bind(this)); + this.hammer.on("dragstart", this._onDragStart.bind(this)); + this.hammer.on("drag", this._onDrag.bind(this)); + this.hammer.on("dragend", this._onDragEnd.bind(this)); // single select (or unselect) when tapping an item - this.hammer.on('tap', this._onSelectItem.bind(this)); + this.hammer.on("tap", this._onSelectItem.bind(this)); // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on('hold', this._onMultiSelectItem.bind(this)); + this.hammer.on("hold", this._onMultiSelectItem.bind(this)); // add item on doubletap - this.hammer.on('doubletap', this._onAddItem.bind(this)); + this.hammer.on("doubletap", this._onAddItem.bind(this)); // attach to the DOM this.show(); @@ -14752,41 +14599,38 @@ return /******/ (function(modules) { // webpackBootstrap * Fired when an item is about to be deleted. * If not implemented, the item will be always removed. */ - ItemSet.prototype.setOptions = function(options) { + ItemSet.prototype.setOptions = function (options) { if (options) { // copy all options that we know - var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap']; + var fields = ["type", "align", "orientation", "padding", "stack", "selectable", "groupOrder", "dataAttributes", "template", "hide", "snap"]; util.selectiveExtend(fields, this.options, options); - if ('margin' in options) { - if (typeof options.margin === 'number') { + if ("margin" in options) { + if (typeof options.margin === "number") { this.options.margin.axis = options.margin; this.options.margin.item.horizontal = options.margin; this.options.margin.item.vertical = options.margin; - } - else if (typeof options.margin === 'object') { - util.selectiveExtend(['axis'], this.options.margin, options.margin); - if ('item' in options.margin) { - if (typeof options.margin.item === 'number') { + } else if (typeof options.margin === "object") { + util.selectiveExtend(["axis"], this.options.margin, options.margin); + if ("item" in options.margin) { + if (typeof options.margin.item === "number") { this.options.margin.item.horizontal = options.margin.item; this.options.margin.item.vertical = options.margin.item; - } - else if (typeof options.margin.item === 'object') { - util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item); + } else if (typeof options.margin.item === "object") { + util.selectiveExtend(["horizontal", "vertical"], this.options.margin.item, options.margin.item); } } } } - if ('editable' in options) { - if (typeof options.editable === 'boolean') { - this.options.editable.updateTime = options.editable; + if ("editable" in options) { + if (typeof options.editable === "boolean") { + this.options.editable.updateTime = options.editable; this.options.editable.updateGroup = options.editable; - this.options.editable.add = options.editable; - this.options.editable.remove = options.editable; - } - else if (typeof options.editable === 'object') { - util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); + this.options.editable.add = options.editable; + this.options.editable.remove = options.editable; + } else if (typeof options.editable === "object") { + util.selectiveExtend(["updateTime", "updateGroup", "add", "remove"], this.options.editable, options.editable); } } @@ -14795,12 +14639,12 @@ return /******/ (function(modules) { // webpackBootstrap var fn = options[name]; if (fn) { if (!(fn instanceof Function)) { - throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)'); + throw new Error("option " + name + " must be a function " + name + "(item, callback)"); } this.options[name] = fn; } }).bind(this); - ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback); + ["onAdd", "onUpdate", "onRemove", "onMove", "onMoving"].forEach(addCallback); // force the itemSet to refresh: options like orientation and margins may be changed this.markDirty(); @@ -14812,7 +14656,7 @@ return /******/ (function(modules) { // webpackBootstrap * Optionally, all items can be marked as dirty and be refreshed. * @param {{refreshItems: boolean}} [options] */ - ItemSet.prototype.markDirty = function(options) { + ItemSet.prototype.markDirty = function (options) { this.groupIds = []; this.stackDirty = true; @@ -14827,7 +14671,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Destroy the ItemSet */ - ItemSet.prototype.destroy = function() { + ItemSet.prototype.destroy = function () { this.hide(); this.setItems(null); this.setGroups(null); @@ -14841,7 +14685,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the component from the DOM */ - ItemSet.prototype.hide = function() { + ItemSet.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); @@ -14862,7 +14706,7 @@ return /******/ (function(modules) { // webpackBootstrap * Show the component in the DOM (when not already visible). * @return {Boolean} changed */ - ItemSet.prototype.show = function() { + ItemSet.prototype.show = function () { // show frame containing the items if (!this.dom.frame.parentNode) { this.body.dom.center.appendChild(this.dom.frame); @@ -14886,7 +14730,7 @@ return /******/ (function(modules) { // webpackBootstrap * selected, or a single item id. If ids is undefined * or an empty array, all items will be unselected. */ - ItemSet.prototype.setSelection = function(ids) { + ItemSet.prototype.setSelection = function (ids) { var i, ii, id, item; if (ids == undefined) ids = []; @@ -14915,7 +14759,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the selected items by their id * @return {Array} ids The ids of the selected items */ - ItemSet.prototype.getSelection = function() { + ItemSet.prototype.getSelection = function () { return this.selection.concat([]); }; @@ -14923,9 +14767,9 @@ return /******/ (function(modules) { // webpackBootstrap * Get the id's of the currently visible items. * @returns {Array} The ids of the visible items */ - ItemSet.prototype.getVisibleItems = function() { + ItemSet.prototype.getVisibleItems = function () { var range = this.body.range.getRange(); - var left = this.body.util.toScreen(range.start); + var left = this.body.util.toScreen(range.start); var right = this.body.util.toScreen(range.end); var ids = []; @@ -14939,7 +14783,7 @@ return /******/ (function(modules) { // webpackBootstrap for (var i = 0; i < rawVisibleItems.length; i++) { var item = rawVisibleItems[i]; // TODO: also check whether visible vertically - if ((item.left < right) && (item.left + item.width > left)) { + if (item.left < right && item.left + item.width > left) { ids.push(item.id); } } @@ -14954,10 +14798,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {String | Number} id * @private */ - ItemSet.prototype._deselect = function(id) { + ItemSet.prototype._deselect = function (id) { var selection = this.selection; for (var i = 0, ii = selection.length; i < ii; i++) { - if (selection[i] == id) { // non-strict comparison! + if (selection[i] == id) { + // non-strict comparison! selection.splice(i, 1); break; } @@ -14968,7 +14813,7 @@ return /******/ (function(modules) { // webpackBootstrap * Repaint the component * @return {boolean} Returns true if the component is resized */ - ItemSet.prototype.redraw = function() { + ItemSet.prototype.redraw = function () { var margin = this.options.margin, range = this.body.range, asSize = util.option.asSize, @@ -14983,7 +14828,7 @@ return /******/ (function(modules) { // webpackBootstrap this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; // update class name - frame.className = 'itemset' + (editable ? ' editable' : ''); + frame.className = "itemset" + (editable ? " editable" : ""); // reorder the groups (if needed) resized = this._orderGroups() || resized; @@ -14991,7 +14836,7 @@ return /******/ (function(modules) { // webpackBootstrap // check whether zoomed (in that case we need to re-stack everything) // TODO: would be nicer to get this as a trigger from Range var visibleInterval = range.end - range.start; - var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth); + var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; if (zoomed) this.stackDirty = true; this.lastVisibleInterval = visibleInterval; this.props.lastWidth = this.props.width; @@ -15014,7 +14859,7 @@ return /******/ (function(modules) { // webpackBootstrap // redraw all regular groups util.forEach(this.groups, function (group) { - var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin; + var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; var groupResized = group.redraw(range, groupMargin, restack); resized = groupResized || resized; height += group.height; @@ -15023,17 +14868,15 @@ return /******/ (function(modules) { // webpackBootstrap this.stackDirty = false; // update frame height - frame.style.height = asSize(height); + frame.style.height = asSize(height); // calculate actual size this.props.width = frame.offsetWidth; this.props.height = height; // reposition axis - this.dom.axis.style.top = asSize((orientation == 'top') ? - (this.body.domProps.top.height + this.body.domProps.border.top) : - (this.body.domProps.top.height + this.body.domProps.centerContainer.height)); - this.dom.axis.style.left = '0'; + this.dom.axis.style.top = asSize(orientation == "top" ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); + this.dom.axis.style.left = "0"; // check if this component is resized resized = this._isResized() || resized; @@ -15046,8 +14889,8 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Group | null} firstGroup * @private */ - ItemSet.prototype._firstGroup = function() { - var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1); + ItemSet.prototype._firstGroup = function () { + var firstGroupIndex = this.options.orientation == "top" ? 0 : this.groupIds.length - 1; var firstGroupId = this.groupIds[firstGroupIndex]; var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; @@ -15059,7 +14902,7 @@ return /******/ (function(modules) { // webpackBootstrap * there are no groups specified. * @protected */ - ItemSet.prototype._updateUngrouped = function() { + ItemSet.prototype._updateUngrouped = function () { var ungrouped = this.groups[UNGROUPED]; var background = this.groups[BACKGROUND]; var item, itemId; @@ -15080,8 +14923,7 @@ return /******/ (function(modules) { // webpackBootstrap } } } - } - else { + } else { // create a group holding all (unfiltered) items if (!ungrouped) { var id = null; @@ -15105,7 +14947,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the element for the labelset * @return {HTMLElement} labelSet */ - ItemSet.prototype.getLabelSet = function() { + ItemSet.prototype.getLabelSet = function () { return this.dom.labelSet; }; @@ -15113,7 +14955,7 @@ return /******/ (function(modules) { // webpackBootstrap * Set items * @param {vis.DataSet | null} items */ - ItemSet.prototype.setItems = function(items) { + ItemSet.prototype.setItems = function (items) { var me = this, ids, oldItemsData = this.itemsData; @@ -15121,12 +14963,10 @@ return /******/ (function(modules) { // webpackBootstrap // replace the dataset if (!items) { this.itemsData = null; - } - else if (items instanceof DataSet || items instanceof DataView) { + } else if (items instanceof DataSet || items instanceof DataView) { this.itemsData = items; - } - else { - throw new TypeError('Data must be an instance of DataSet or DataView'); + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); } if (oldItemsData) { @@ -15160,7 +15000,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the current items * @returns {vis.DataSet | null} */ - ItemSet.prototype.getItems = function() { + ItemSet.prototype.getItems = function () { return this.itemsData; }; @@ -15168,7 +15008,7 @@ return /******/ (function(modules) { // webpackBootstrap * Set groups * @param {vis.DataSet} groups */ - ItemSet.prototype.setGroups = function(groups) { + ItemSet.prototype.setGroups = function (groups) { var me = this, ids; @@ -15187,12 +15027,10 @@ return /******/ (function(modules) { // webpackBootstrap // replace the dataset if (!groups) { this.groupsData = null; - } - else if (groups instanceof DataSet || groups instanceof DataView) { + } else if (groups instanceof DataSet || groups instanceof DataView) { this.groupsData = groups; - } - else { - throw new TypeError('Data must be an instance of DataSet or DataView'); + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); } if (this.groupsData) { @@ -15213,14 +15051,14 @@ return /******/ (function(modules) { // webpackBootstrap // update the order of all items in each group this._order(); - this.body.emitter.emit('change', {queue: true}); + this.body.emitter.emit("change", { queue: true }); }; /** * Get the current groups * @returns {vis.DataSet | null} groups */ - ItemSet.prototype.getGroups = function() { + ItemSet.prototype.getGroups = function () { return this.groupsData; }; @@ -15228,7 +15066,7 @@ return /******/ (function(modules) { // webpackBootstrap * Remove an item by its id * @param {String | Number} id */ - ItemSet.prototype.removeItem = function(id) { + ItemSet.prototype.removeItem = function (id) { var item = this.itemsData.get(id), dataset = this.itemsData.getDataSet(); @@ -15251,7 +15089,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ ItemSet.prototype._getType = function (itemData) { - return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); + return itemData.type || this.options.type || (itemData.end ? "range" : "box"); }; @@ -15263,10 +15101,9 @@ return /******/ (function(modules) { // webpackBootstrap */ ItemSet.prototype._getGroupId = function (itemData) { var type = this._getType(itemData); - if (type == 'background' && itemData.group == undefined) { - return BACKGROUND; - } - else { + if (type == "background" && itemData.group == undefined) { + return BACKGROUND; + } else { return this.groupsData ? itemData.group : UNGROUPED; } }; @@ -15276,7 +15113,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number[]} ids * @protected */ - ItemSet.prototype._onUpdate = function(ids) { + ItemSet.prototype._onUpdate = function (ids) { var me = this; ids.forEach(function (id) { @@ -15292,8 +15129,7 @@ return /******/ (function(modules) { // webpackBootstrap // item type has changed, delete the item and recreate it me._removeItem(item); item = null; - } - else { + } else { me._updateItem(item, itemData); } } @@ -15304,21 +15140,18 @@ return /******/ (function(modules) { // webpackBootstrap item = new constructor(itemData, me.conversion, me.options); item.id = id; // TODO: not so nice setting id afterwards me._addItem(item); - } - else if (type == 'rangeoverflow') { + } else if (type == "rangeoverflow") { // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day - throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' + - '.vis.timeline .item.range .content {overflow: visible;}'); - } - else { - throw new TypeError('Unknown item type "' + type + '"'); + throw new TypeError("Item type \"rangeoverflow\" is deprecated. Use css styling instead: " + ".vis.timeline .item.range .content {overflow: visible;}"); + } else { + throw new TypeError("Unknown item type \"" + type + "\""); } } }); this._order(); this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', {queue: true}); + this.body.emitter.emit("change", { queue: true }); }; /** @@ -15333,7 +15166,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number[]} ids * @protected */ - ItemSet.prototype._onRemove = function(ids) { + ItemSet.prototype._onRemove = function (ids) { var count = 0; var me = this; ids.forEach(function (id) { @@ -15348,7 +15181,7 @@ return /******/ (function(modules) { // webpackBootstrap // update order this._order(); this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', {queue: true}); + this.body.emitter.emit("change", { queue: true }); } }; @@ -15356,7 +15189,7 @@ return /******/ (function(modules) { // webpackBootstrap * Update the order of item in all groups * @private */ - ItemSet.prototype._order = function() { + ItemSet.prototype._order = function () { // reorder the items in all groups // TODO: optimization: only reorder groups affected by the changed items util.forEach(this.groups, function (group) { @@ -15369,7 +15202,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number[]} ids * @private */ - ItemSet.prototype._onUpdateGroups = function(ids) { + ItemSet.prototype._onUpdateGroups = function (ids) { this._onAddGroups(ids); }; @@ -15378,7 +15211,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number[]} ids * @private */ - ItemSet.prototype._onAddGroups = function(ids) { + ItemSet.prototype._onAddGroups = function (ids) { var me = this; ids.forEach(function (id) { @@ -15388,7 +15221,7 @@ return /******/ (function(modules) { // webpackBootstrap if (!group) { // check for reserved ids if (id == UNGROUPED || id == BACKGROUND) { - throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + throw new Error("Illegal group id. " + id + " is a reserved id."); } var groupOptions = Object.create(me.options); @@ -15411,14 +15244,13 @@ return /******/ (function(modules) { // webpackBootstrap group.order(); group.show(); - } - else { + } else { // update group group.setData(groupData); } }); - this.body.emitter.emit('change', {queue: true}); + this.body.emitter.emit("change", { queue: true }); }; /** @@ -15426,7 +15258,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number[]} ids * @private */ - ItemSet.prototype._onRemoveGroups = function(ids) { + ItemSet.prototype._onRemoveGroups = function (ids) { var groups = this.groups; ids.forEach(function (id) { var group = groups[id]; @@ -15439,7 +15271,7 @@ return /******/ (function(modules) { // webpackBootstrap this.markDirty(); - this.body.emitter.emit('change', {queue: true}); + this.body.emitter.emit("change", { queue: true }); }; /** @@ -15471,8 +15303,7 @@ return /******/ (function(modules) { // webpackBootstrap } return changed; - } - else { + } else { return false; } }; @@ -15482,7 +15313,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Item} item * @private */ - ItemSet.prototype._addItem = function(item) { + ItemSet.prototype._addItem = function (item) { this.items[item.id] = item; // add to group @@ -15497,7 +15328,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} itemData * @private */ - ItemSet.prototype._updateItem = function(item, itemData) { + ItemSet.prototype._updateItem = function (item, itemData) { var oldGroupId = item.data.group; // update the items data (will redraw the item when displayed) @@ -15520,7 +15351,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Item} item * @private */ - ItemSet.prototype._removeItem = function(item) { + ItemSet.prototype._removeItem = function (item) { // remove from DOM item.hide(); @@ -15541,7 +15372,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {Array} * @private */ - ItemSet.prototype._constructByEndArray = function(array) { + ItemSet.prototype._constructByEndArray = function (array) { var endArray = []; for (var i = 0; i < array.length; i++) { @@ -15594,12 +15425,11 @@ return /******/ (function(modules) { // webpackBootstrap props.start = item.data.start.valueOf(); } if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; + if ("group" in item.data) props.group = item.data.group; } this.touchParams.itemProps = [props]; - } - else if (dragRightItem) { + } else if (dragRightItem) { props = { item: dragRightItem, initialX: event.gesture.center.clientX @@ -15609,12 +15439,11 @@ return /******/ (function(modules) { // webpackBootstrap props.end = item.data.end.valueOf(); } if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; + if ("group" in item.data) props.group = item.data.group; } this.touchParams.itemProps = [props]; - } - else { + } else { this.touchParams.itemProps = this.getSelection().map(function (id) { var item = me.items[id]; var props = { @@ -15623,10 +15452,10 @@ return /******/ (function(modules) { // webpackBootstrap }; if (me.options.editable.updateTime) { - if ('start' in item.data) { + if ("start" in item.data) { props.start = item.data.start.valueOf(); - if ('end' in item.data) { + if ("end" in item.data) { // we store a duration here in order not to change the width // of the item when moving it. props.duration = item.data.end.valueOf() - props.start; @@ -15634,7 +15463,7 @@ return /******/ (function(modules) { // webpackBootstrap } } if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; + if ("group" in item.data) props.group = item.data.group; } return props; @@ -15667,20 +15496,19 @@ return /******/ (function(modules) { // webpackBootstrap var initial = me.body.util.toTime(props.initialX - xOffset); var offset = current - initial; - if ('start' in props) { + if ("start" in props) { var start = new Date(props.start + offset); newProps.start = snap ? snap(start, scale, step) : start; } - if ('end' in props) { + if ("end" in props) { var end = new Date(props.end + offset); newProps.end = snap ? snap(end, scale, step) : end; - } - else if ('duration' in props) { + } else if ("duration" in props) { newProps.end = new Date(newProps.start.valueOf() + props.duration); } - if ('group' in props) { + if ("group" in props) { // drag from one group to another var group = me.groupFromTarget(event); newProps.group = group && group.groupId; @@ -15696,7 +15524,7 @@ return /******/ (function(modules) { // webpackBootstrap }); this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + this.body.emitter.emit("change"); event.stopPropagation(); } @@ -15708,12 +15536,12 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} props Can contain properties start, end, and group. * @private */ - ItemSet.prototype._updateItemProps = function(item, props) { + ItemSet.prototype._updateItemProps = function (item, props) { // TODO: copy all properties from props to item? (also new ones) - if ('start' in props) item.data.start = props.start; - if ('end' in props) item.data.end = props.end; - if ('group' in props && item.data.group != props.group) { - this._moveToGroup(item, props.group) + if ("start" in props) item.data.start = props.start; + if ("end" in props) item.data.end = props.end; + if ("group" in props && item.data.group != props.group) { + this._moveToGroup(item, props.group); } }; @@ -15723,7 +15551,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {String | Number} groupId * @private */ - ItemSet.prototype._moveToGroup = function(item, groupId) { + ItemSet.prototype._moveToGroup = function (item, groupId) { var group = this.groups[groupId]; if (group && group.groupId != item.data.group) { var oldGroup = item.parent; @@ -15742,7 +15570,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ ItemSet.prototype._onDragEnd = function (event) { - event.preventDefault() + event.preventDefault(); if (this.touchParams.itemProps) { // prepare a change set for the changed items @@ -15750,25 +15578,23 @@ return /******/ (function(modules) { // webpackBootstrap me = this, dataset = this.itemsData.getDataSet(); - var itemProps = this.touchParams.itemProps ; + var itemProps = this.touchParams.itemProps; this.touchParams.itemProps = null; itemProps.forEach(function (props) { var id = props.item.id, itemData = me.itemsData.get(id, me.itemOptions); var changed = false; - if ('start' in props.item.data) { - changed = (props.start != props.item.data.start.valueOf()); - itemData.start = util.convert(props.item.data.start, - dataset._options.type && dataset._options.type.start || 'Date'); - } - if ('end' in props.item.data) { - changed = changed || (props.end != props.item.data.end.valueOf()); - itemData.end = util.convert(props.item.data.end, - dataset._options.type && dataset._options.type.end || 'Date'); - } - if ('group' in props.item.data) { - changed = changed || (props.group != props.item.data.group); + if ("start" in props.item.data) { + changed = props.start != props.item.data.start.valueOf(); + itemData.start = util.convert(props.item.data.start, dataset._options.type && dataset._options.type.start || "Date"); + } + if ("end" in props.item.data) { + changed = changed || props.end != props.item.data.end.valueOf(); + itemData.end = util.convert(props.item.data.end, dataset._options.type && dataset._options.type.end || "Date"); + } + if ("group" in props.item.data) { + changed = changed || props.group != props.item.data.group; itemData.group = props.item.data.group; } @@ -15779,13 +15605,12 @@ return /******/ (function(modules) { // webpackBootstrap // apply changes itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) changes.push(itemData); - } - else { + } else { // restore original values me._updateItemProps(props.item, props); me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit('change'); + me.body.emitter.emit("change"); } }); } @@ -15808,7 +15633,7 @@ return /******/ (function(modules) { // webpackBootstrap ItemSet.prototype._onSelectItem = function (event) { if (!this.options.selectable) return; - var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey; + var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey; var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey; if (ctrlKey || shiftKey) { this._onMultiSelectItem(event); @@ -15826,7 +15651,7 @@ return /******/ (function(modules) { // webpackBootstrap // emit a select event, // except when old selection is empty and new selection is still empty if (newSelection.length > 0 || oldSelection.length > 0) { - this.body.emitter.emit('select', { + this.body.emitter.emit("select", { items: newSelection }); } @@ -15855,8 +15680,7 @@ return /******/ (function(modules) { // webpackBootstrap me.itemsData.getDataSet().update(itemData); } }); - } - else { + } else { // add item var xAbs = util.getAbsoluteLeft(this.dom.frame); var x = event.gesture.center.pageX - xAbs; @@ -15866,11 +15690,11 @@ return /******/ (function(modules) { // webpackBootstrap var newItem = { start: snap ? snap(start, scale, step) : start, - content: 'new item' + content: "new item" }; // when default type is a range, add a default end date to the new item - if (this.options.type === 'range') { + if (this.options.type === "range") { var end = this.body.util.toTime(x + this.props.width / 5); newItem.end = snap ? snap(end, scale, step) : end; } @@ -15921,22 +15745,20 @@ return /******/ (function(modules) { // webpackBootstrap if (this.items.hasOwnProperty(id)) { var _item = this.items[id]; var start = _item.data.start; - var end = (_item.data.end !== undefined) ? _item.data.end : start; + var end = _item.data.end !== undefined ? _item.data.end : start; if (start >= range.min && end <= range.max) { selection.push(_item.id); // do not use id but item.id, id itself is stringified } } } - } - else { + } else { // add/remove this item from the current selection var index = selection.indexOf(item.id); if (index == -1) { // item is not yet selected -> select it selection.push(item.id); - } - else { + } else { // item is already selected -> deselect it selection.splice(index, 1); } @@ -15944,7 +15766,7 @@ return /******/ (function(modules) { // webpackBootstrap this.setSelection(selection); - this.body.emitter.emit('select', { + this.body.emitter.emit("select", { items: this.getSelection() }); } @@ -15956,7 +15778,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {{min: Date, max: Date}} Returns the range of the provided items * @private */ - ItemSet._getItemRange = function(itemsData) { + ItemSet._getItemRange = function (itemsData) { var max = null; var min = null; @@ -15969,8 +15791,7 @@ return /******/ (function(modules) { // webpackBootstrap if (max == null || data.end > max) { max = data.end; } - } - else { + } else { if (max == null || data.start > max) { max = data.start; } @@ -15980,7 +15801,7 @@ return /******/ (function(modules) { // webpackBootstrap return { min: min, max: max - } + }; }; /** @@ -15989,11 +15810,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @return {Item | null} item */ - ItemSet.itemFromTarget = function(event) { + ItemSet.itemFromTarget = function (event) { var target = event.target; while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; + if (target.hasOwnProperty("timeline-item")) { + return target["timeline-item"]; } target = target.parentNode; } @@ -16007,7 +15828,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @return {Group | null} group */ - ItemSet.prototype.groupFromTarget = function(event) { + ItemSet.prototype.groupFromTarget = function (event) { // TODO: cleanup when the new solution is stable (also on mobile) //var target = event.target; //while (target) { @@ -16028,12 +15849,11 @@ return /******/ (function(modules) { // webpackBootstrap return group; } - if (this.options.orientation === 'top') { + if (this.options.orientation === "top") { if (i === this.groupIds.length - 1 && clientY > top) { return group; } - } - else { + } else { if (i === 0 && clientY < top + foreground.offset) { return group; } @@ -16049,11 +15869,11 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @return {ItemSet | null} item */ - ItemSet.itemSetFromTarget = function(event) { + ItemSet.itemSetFromTarget = function (event) { var target = event.target; while (target) { - if (target.hasOwnProperty('timeline-itemset')) { - return target['timeline-itemset']; + if (target.hasOwnProperty("timeline-itemset")) { + return target["timeline-itemset"]; } target = target.parentNode; } @@ -16063,11 +15883,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = ItemSet; - /***/ }, /* 27 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var moment = __webpack_require__(2); var DateUtil = __webpack_require__(24); var util = __webpack_require__(1); @@ -16104,8 +15925,8 @@ return /******/ (function(modules) { // webpackBootstrap this._start = new Date(); this._end = new Date(); - this.autoScale = true; - this.scale = 'day'; + this.autoScale = true; + this.scale = "day"; this.step = 1; // initialize the range @@ -16126,24 +15947,24 @@ return /******/ (function(modules) { // webpackBootstrap // Time formatting TimeStep.FORMAT = { minorLabels: { - millisecond:'SSS', - second: 's', - minute: 'HH:mm', - hour: 'HH:mm', - weekday: 'ddd D', - day: 'D', - month: 'MMM', - year: 'YYYY' + millisecond: "SSS", + second: "s", + minute: "HH:mm", + hour: "HH:mm", + weekday: "ddd D", + day: "D", + month: "MMM", + year: "YYYY" }, majorLabels: { - millisecond:'HH:mm:ss', - second: 'D MMMM HH:mm', - minute: 'ddd D MMMM', - hour: 'ddd D MMMM', - weekday: 'MMMM YYYY', - day: 'MMMM YYYY', - month: 'YYYY', - year: '' + millisecond: "HH:mm:ss", + second: "D MMMM HH:mm", + minute: "ddd D MMMM", + hour: "ddd D MMMM", + weekday: "MMMM YYYY", + day: "MMMM YYYY", + month: "YYYY", + year: "" } }; @@ -16168,13 +15989,13 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Date} [end] The end date and time. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds */ - TimeStep.prototype.setRange = function(start, end, minimumStep) { + TimeStep.prototype.setRange = function (start, end, minimumStep) { if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; + throw "No legal start or end date in method setRange"; } - this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); - this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + this._start = start != undefined ? new Date(start.valueOf()) : new Date(); + this._end = end != undefined ? new Date(end.valueOf()) : new Date(); if (this.autoScale) { this.setMinimumStep(minimumStep); @@ -16184,7 +16005,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Set the range iterator to the start date. */ - TimeStep.prototype.first = function() { + TimeStep.prototype.first = function () { this.current = new Date(this._start.valueOf()); this.roundToMinor(); }; @@ -16193,35 +16014,50 @@ return /******/ (function(modules) { // webpackBootstrap * Round the current date to the first minor date value * This must be executed once when the current date is set to start Date */ - TimeStep.prototype.roundToMinor = function() { + TimeStep.prototype.roundToMinor = function () { // round to floor // IMPORTANT: we have no breaks in this switch! (this is no bug) // noinspection FallThroughInSwitchStatementJS switch (this.scale) { - case 'year': + case "year": this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); this.current.setMonth(0); - case 'month': this.current.setDate(1); - case 'day': // intentional fall through - case 'weekday': this.current.setHours(0); - case 'hour': this.current.setMinutes(0); - case 'minute': this.current.setSeconds(0); - case 'second': this.current.setMilliseconds(0); - //case 'millisecond': // nothing to do for milliseconds + case "month": + this.current.setDate(1); + case "day": + // intentional fall through + case "weekday": + this.current.setHours(0); + case "hour": + this.current.setMinutes(0); + case "minute": + this.current.setSeconds(0); + case "second": + this.current.setMilliseconds(0); + //case 'millisecond': // nothing to do for milliseconds } if (this.step != 1) { // round down to the first minor value that is a multiple of the current step size switch (this.scale) { - case 'millisecond': this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; - case 'second': this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; - case 'minute': this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; - case 'hour': this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; - case 'weekday': // intentional fall through - case 'day': this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; - case 'month': this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; - case 'year': this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; - default: break; + case "millisecond": + this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);break; + case "second": + this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);break; + case "minute": + this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);break; + case "hour": + this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);break; + case "weekday": + // intentional fall through + case "day": + this.current.setDate(this.current.getDate() - 1 - (this.current.getDate() - 1) % this.step + 1);break; + case "month": + this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);break; + case "year": + this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step);break; + default: + break; } } }; @@ -16231,63 +16067,88 @@ return /******/ (function(modules) { // webpackBootstrap * @return {boolean} true if the current date has not passed the end date */ TimeStep.prototype.hasNext = function () { - return (this.current.valueOf() <= this._end.valueOf()); + return this.current.valueOf() <= this._end.valueOf(); }; /** * Do the next step */ - TimeStep.prototype.next = function() { + TimeStep.prototype.next = function () { var prev = this.current.valueOf(); // Two cases, needed to prevent issues with switching daylight savings // (end of March and end of October) - if (this.current.getMonth() < 6) { + if (this.current.getMonth() < 6) { switch (this.scale) { - case 'millisecond': + case "millisecond": + - this.current = new Date(this.current.valueOf() + this.step); break; - case 'second': this.current = new Date(this.current.valueOf() + this.step * 1000); break; - case 'minute': this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; - case 'hour': + this.current = new Date(this.current.valueOf() + this.step);break; + case "second": + this.current = new Date(this.current.valueOf() + this.step * 1000);break; + case "minute": + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; + case "hour": this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) var h = this.current.getHours(); - this.current.setHours(h - (h % this.step)); + this.current.setHours(h - h % this.step); + break; + case "weekday": + // intentional fall through + case "day": + this.current.setDate(this.current.getDate() + this.step);break; + case "month": + this.current.setMonth(this.current.getMonth() + this.step);break; + case "year": + this.current.setFullYear(this.current.getFullYear() + this.step);break; + default: break; - case 'weekday': // intentional fall through - case 'day': this.current.setDate(this.current.getDate() + this.step); break; - case 'month': this.current.setMonth(this.current.getMonth() + this.step); break; - case 'year': this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; } - } - else { + } else { switch (this.scale) { - case 'millisecond': this.current = new Date(this.current.valueOf() + this.step); break; - case 'second': this.current.setSeconds(this.current.getSeconds() + this.step); break; - case 'minute': this.current.setMinutes(this.current.getMinutes() + this.step); break; - case 'hour': this.current.setHours(this.current.getHours() + this.step); break; - case 'weekday': // intentional fall through - case 'day': this.current.setDate(this.current.getDate() + this.step); break; - case 'month': this.current.setMonth(this.current.getMonth() + this.step); break; - case 'year': this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; + case "millisecond": + this.current = new Date(this.current.valueOf() + this.step);break; + case "second": + this.current.setSeconds(this.current.getSeconds() + this.step);break; + case "minute": + this.current.setMinutes(this.current.getMinutes() + this.step);break; + case "hour": + this.current.setHours(this.current.getHours() + this.step);break; + case "weekday": + // intentional fall through + case "day": + this.current.setDate(this.current.getDate() + this.step);break; + case "month": + this.current.setMonth(this.current.getMonth() + this.step);break; + case "year": + this.current.setFullYear(this.current.getFullYear() + this.step);break; + default: + break; } } if (this.step != 1) { // round down to the correct major value switch (this.scale) { - case 'millisecond': if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; - case 'second': if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; - case 'minute': if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; - case 'hour': if(this.current.getHours() < this.step) this.current.setHours(0); break; - case 'weekday': // intentional fall through - case 'day': if(this.current.getDate() < this.step+1) this.current.setDate(1); break; - case 'month': if(this.current.getMonth() < this.step) this.current.setMonth(0); break; - case 'year': break; // nothing to do for year - default: break; + case "millisecond": + if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; + case "second": + if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; + case "minute": + if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; + case "hour": + if (this.current.getHours() < this.step) this.current.setHours(0);break; + case "weekday": + // intentional fall through + case "day": + if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; + case "month": + if (this.current.getMonth() < this.step) this.current.setMonth(0);break; + case "year": + break; // nothing to do for year + default: + break; } } @@ -16304,7 +16165,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the current datetime * @return {Date} current The current date */ - TimeStep.prototype.getCurrent = function() { + TimeStep.prototype.getCurrent = function () { return this.current; }; @@ -16320,8 +16181,8 @@ return /******/ (function(modules) { // webpackBootstrap * - A number 'step'. A step size, by default 1. * Choose for example 1, 2, 5, or 10. */ - TimeStep.prototype.setScale = function(params) { - if (params && typeof params.scale == 'string') { + TimeStep.prototype.setScale = function (params) { + if (params && typeof params.scale == "string") { this.scale = params.scale; this.step = params.step > 0 ? params.step : 1; this.autoScale = false; @@ -16341,51 +16202,109 @@ return /******/ (function(modules) { // webpackBootstrap * Automatically determine the scale that bests fits the provided minimum step * @param {Number} [minimumStep] The minimum step size in milliseconds */ - TimeStep.prototype.setMinimumStep = function(minimumStep) { + TimeStep.prototype.setMinimumStep = function (minimumStep) { if (minimumStep == undefined) { return; } //var b = asc + ds; - var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); - var stepMonth = (1000 * 60 * 60 * 24 * 30); - var stepDay = (1000 * 60 * 60 * 24); - var stepHour = (1000 * 60 * 60); - var stepMinute = (1000 * 60); - var stepSecond = (1000); - var stepMillisecond= (1); + var stepYear = 1000 * 60 * 60 * 24 * 30 * 12; + var stepMonth = 1000 * 60 * 60 * 24 * 30; + var stepDay = 1000 * 60 * 60 * 24; + var stepHour = 1000 * 60 * 60; + var stepMinute = 1000 * 60; + var stepSecond = 1000; + var stepMillisecond = 1; // find the smallest step that is larger than the provided minimumStep - if (stepYear*1000 > minimumStep) {this.scale = 'year'; this.step = 1000;} - if (stepYear*500 > minimumStep) {this.scale = 'year'; this.step = 500;} - if (stepYear*100 > minimumStep) {this.scale = 'year'; this.step = 100;} - if (stepYear*50 > minimumStep) {this.scale = 'year'; this.step = 50;} - if (stepYear*10 > minimumStep) {this.scale = 'year'; this.step = 10;} - if (stepYear*5 > minimumStep) {this.scale = 'year'; this.step = 5;} - if (stepYear > minimumStep) {this.scale = 'year'; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = 'month'; this.step = 3;} - if (stepMonth > minimumStep) {this.scale = 'month'; this.step = 1;} - if (stepDay*5 > minimumStep) {this.scale = 'day'; this.step = 5;} - if (stepDay*2 > minimumStep) {this.scale = 'day'; this.step = 2;} - if (stepDay > minimumStep) {this.scale = 'day'; this.step = 1;} - if (stepDay/2 > minimumStep) {this.scale = 'weekday'; this.step = 1;} - if (stepHour*4 > minimumStep) {this.scale = 'hour'; this.step = 4;} - if (stepHour > minimumStep) {this.scale = 'hour'; this.step = 1;} - if (stepMinute*15 > minimumStep) {this.scale = 'minute'; this.step = 15;} - if (stepMinute*10 > minimumStep) {this.scale = 'minute'; this.step = 10;} - if (stepMinute*5 > minimumStep) {this.scale = 'minute'; this.step = 5;} - if (stepMinute > minimumStep) {this.scale = 'minute'; this.step = 1;} - if (stepSecond*15 > minimumStep) {this.scale = 'second'; this.step = 15;} - if (stepSecond*10 > minimumStep) {this.scale = 'second'; this.step = 10;} - if (stepSecond*5 > minimumStep) {this.scale = 'second'; this.step = 5;} - if (stepSecond > minimumStep) {this.scale = 'second'; this.step = 1;} - if (stepMillisecond*200 > minimumStep) {this.scale = 'millisecond'; this.step = 200;} - if (stepMillisecond*100 > minimumStep) {this.scale = 'millisecond'; this.step = 100;} - if (stepMillisecond*50 > minimumStep) {this.scale = 'millisecond'; this.step = 50;} - if (stepMillisecond*10 > minimumStep) {this.scale = 'millisecond'; this.step = 10;} - if (stepMillisecond*5 > minimumStep) {this.scale = 'millisecond'; this.step = 5;} - if (stepMillisecond > minimumStep) {this.scale = 'millisecond'; this.step = 1;} + if (stepYear * 1000 > minimumStep) { + this.scale = "year";this.step = 1000; + } + if (stepYear * 500 > minimumStep) { + this.scale = "year";this.step = 500; + } + if (stepYear * 100 > minimumStep) { + this.scale = "year";this.step = 100; + } + if (stepYear * 50 > minimumStep) { + this.scale = "year";this.step = 50; + } + if (stepYear * 10 > minimumStep) { + this.scale = "year";this.step = 10; + } + if (stepYear * 5 > minimumStep) { + this.scale = "year";this.step = 5; + } + if (stepYear > minimumStep) { + this.scale = "year";this.step = 1; + } + if (stepMonth * 3 > minimumStep) { + this.scale = "month";this.step = 3; + } + if (stepMonth > minimumStep) { + this.scale = "month";this.step = 1; + } + if (stepDay * 5 > minimumStep) { + this.scale = "day";this.step = 5; + } + if (stepDay * 2 > minimumStep) { + this.scale = "day";this.step = 2; + } + if (stepDay > minimumStep) { + this.scale = "day";this.step = 1; + } + if (stepDay / 2 > minimumStep) { + this.scale = "weekday";this.step = 1; + } + if (stepHour * 4 > minimumStep) { + this.scale = "hour";this.step = 4; + } + if (stepHour > minimumStep) { + this.scale = "hour";this.step = 1; + } + if (stepMinute * 15 > minimumStep) { + this.scale = "minute";this.step = 15; + } + if (stepMinute * 10 > minimumStep) { + this.scale = "minute";this.step = 10; + } + if (stepMinute * 5 > minimumStep) { + this.scale = "minute";this.step = 5; + } + if (stepMinute > minimumStep) { + this.scale = "minute";this.step = 1; + } + if (stepSecond * 15 > minimumStep) { + this.scale = "second";this.step = 15; + } + if (stepSecond * 10 > minimumStep) { + this.scale = "second";this.step = 10; + } + if (stepSecond * 5 > minimumStep) { + this.scale = "second";this.step = 5; + } + if (stepSecond > minimumStep) { + this.scale = "second";this.step = 1; + } + if (stepMillisecond * 200 > minimumStep) { + this.scale = "millisecond";this.step = 200; + } + if (stepMillisecond * 100 > minimumStep) { + this.scale = "millisecond";this.step = 100; + } + if (stepMillisecond * 50 > minimumStep) { + this.scale = "millisecond";this.step = 50; + } + if (stepMillisecond * 10 > minimumStep) { + this.scale = "millisecond";this.step = 10; + } + if (stepMillisecond * 5 > minimumStep) { + this.scale = "millisecond";this.step = 5; + } + if (stepMillisecond > minimumStep) { + this.scale = "millisecond";this.step = 1; + } }; /** @@ -16398,10 +16317,10 @@ return /******/ (function(modules) { // webpackBootstrap * @param {number} step Current step (1, 2, 4, 5, ... * @return {Date} snappedDate */ - TimeStep.snap = function(date, scale, step) { + TimeStep.snap = function (date, scale, step) { var clone = new Date(date.valueOf()); - if (scale == 'year') { + if (scale == "year") { var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); clone.setFullYear(Math.round(year / step) * step); clone.setMonth(0); @@ -16410,14 +16329,12 @@ return /******/ (function(modules) { // webpackBootstrap clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); - } - else if (scale == 'month') { + } else if (scale == "month") { if (clone.getDate() > 15) { clone.setDate(1); clone.setMonth(clone.getMonth() + 1); // important: first set Date to 1, after that change the month. - } - else { + } else { clone.setDate(1); } @@ -16425,43 +16342,40 @@ return /******/ (function(modules) { // webpackBootstrap clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); - } - else if (scale == 'day') { + } else if (scale == "day") { //noinspection FallthroughInSwitchStatementJS switch (step) { case 5: case 2: - clone.setHours(Math.round(clone.getHours() / 24) * 24); break; + clone.setHours(Math.round(clone.getHours() / 24) * 24);break; default: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; + clone.setHours(Math.round(clone.getHours() / 12) * 12);break; } clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); - } - else if (scale == 'weekday') { + } else if (scale == "weekday") { //noinspection FallthroughInSwitchStatementJS switch (step) { case 5: case 2: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; + clone.setHours(Math.round(clone.getHours() / 12) * 12);break; default: - clone.setHours(Math.round(clone.getHours() / 6) * 6); break; + clone.setHours(Math.round(clone.getHours() / 6) * 6);break; } clone.setMinutes(0); clone.setSeconds(0); clone.setMilliseconds(0); - } - else if (scale == 'hour') { + } else if (scale == "hour") { switch (step) { case 4: - clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60);break; default: - clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30);break; } clone.setSeconds(0); clone.setMilliseconds(0); - } else if (scale == 'minute') { + } else if (scale == "minute") { //noinspection FallthroughInSwitchStatementJS switch (step) { case 15: @@ -16470,13 +16384,12 @@ return /******/ (function(modules) { // webpackBootstrap clone.setSeconds(0); break; case 5: - clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60);break; default: - clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30);break; } clone.setMilliseconds(0); - } - else if (scale == 'second') { + } else if (scale == "second") { //noinspection FallthroughInSwitchStatementJS switch (step) { case 15: @@ -16485,16 +16398,15 @@ return /******/ (function(modules) { // webpackBootstrap clone.setMilliseconds(0); break; case 5: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000);break; default: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500);break; } - } - else if (scale == 'millisecond') { + } else if (scale == "millisecond") { var _step = step > 5 ? step / 2 : 1; clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); } - + return clone; }; @@ -16503,44 +16415,42 @@ return /******/ (function(modules) { // webpackBootstrap * is DAY, a major value is each first day of the MONTH) * @return {boolean} true if current date is major, else false. */ - TimeStep.prototype.isMajor = function() { + TimeStep.prototype.isMajor = function () { if (this.switchedYear == true) { this.switchedYear = false; switch (this.scale) { - case 'year': - case 'month': - case 'weekday': - case 'day': - case 'hour': - case 'minute': - case 'second': - case 'millisecond': + case "year": + case "month": + case "weekday": + case "day": + case "hour": + case "minute": + case "second": + case "millisecond": return true; default: return false; } - } - else if (this.switchedMonth == true) { + } else if (this.switchedMonth == true) { this.switchedMonth = false; switch (this.scale) { - case 'weekday': - case 'day': - case 'hour': - case 'minute': - case 'second': - case 'millisecond': + case "weekday": + case "day": + case "hour": + case "minute": + case "second": + case "millisecond": return true; default: return false; } - } - else if (this.switchedDay == true) { + } else if (this.switchedDay == true) { this.switchedDay = false; switch (this.scale) { - case 'millisecond': - case 'second': - case 'minute': - case 'hour': + case "millisecond": + case "second": + case "minute": + case "hour": return true; default: return false; @@ -16548,20 +16458,21 @@ return /******/ (function(modules) { // webpackBootstrap } switch (this.scale) { - case 'millisecond': - return (this.current.getMilliseconds() == 0); - case 'second': - return (this.current.getSeconds() == 0); - case 'minute': - return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); - case 'hour': - return (this.current.getHours() == 0); - case 'weekday': // intentional fall through - case 'day': - return (this.current.getDate() == 1); - case 'month': - return (this.current.getMonth() == 0); - case 'year': + case "millisecond": + return this.current.getMilliseconds() == 0; + case "second": + return this.current.getSeconds() == 0; + case "minute": + return this.current.getHours() == 0 && this.current.getMinutes() == 0; + case "hour": + return this.current.getHours() == 0; + case "weekday": + // intentional fall through + case "day": + return this.current.getDate() == 1; + case "month": + return this.current.getMonth() == 0; + case "year": return false; default: return false; @@ -16575,13 +16486,13 @@ return /******/ (function(modules) { // webpackBootstrap * formatted as "hh:mm". * @param {Date} [date] custom date. if not provided, current date is taken */ - TimeStep.prototype.getLabelMinor = function(date) { + TimeStep.prototype.getLabelMinor = function (date) { if (date == undefined) { date = this.current; } var format = this.format.minorLabels[this.scale]; - return (format && format.length > 0) ? moment(date).format(format) : ''; + return format && format.length > 0 ? moment(date).format(format) : ""; }; /** @@ -16590,95 +16501,94 @@ return /******/ (function(modules) { // webpackBootstrap * hours, and the hour will be formatted as "hh". * @param {Date} [date] custom date. if not provided, current date is taken */ - TimeStep.prototype.getLabelMajor = function(date) { + TimeStep.prototype.getLabelMajor = function (date) { if (date == undefined) { date = this.current; } var format = this.format.majorLabels[this.scale]; - return (format && format.length > 0) ? moment(date).format(format) : ''; + return format && format.length > 0 ? moment(date).format(format) : ""; }; - TimeStep.prototype.getClassName = function() { + TimeStep.prototype.getClassName = function () { var m = moment(this.current); - var date = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function + var date = m.locale ? m.locale("en") : m.lang("en"); // old versions of moment have .lang() function var step = this.step; function even(value) { - return (value / step % 2 == 0) ? ' even' : ' odd'; + return value / step % 2 == 0 ? " even" : " odd"; } function today(date) { - if (date.isSame(new Date(), 'day')) { - return ' today'; + if (date.isSame(new Date(), "day")) { + return " today"; } - if (date.isSame(moment().add(1, 'day'), 'day')) { - return ' tomorrow'; + if (date.isSame(moment().add(1, "day"), "day")) { + return " tomorrow"; } - if (date.isSame(moment().add(-1, 'day'), 'day')) { - return ' yesterday'; + if (date.isSame(moment().add(-1, "day"), "day")) { + return " yesterday"; } - return ''; + return ""; } function currentWeek(date) { - return date.isSame(new Date(), 'week') ? ' current-week' : ''; + return date.isSame(new Date(), "week") ? " current-week" : ""; } function currentMonth(date) { - return date.isSame(new Date(), 'month') ? ' current-month' : ''; + return date.isSame(new Date(), "month") ? " current-month" : ""; } function currentYear(date) { - return date.isSame(new Date(), 'year') ? ' current-year' : ''; + return date.isSame(new Date(), "year") ? " current-year" : ""; } switch (this.scale) { - case 'millisecond': + case "millisecond": return even(date.milliseconds()).trim(); - case 'second': + case "second": return even(date.seconds()).trim(); - case 'minute': + case "minute": return even(date.minutes()).trim(); - case 'hour': + case "hour": var hours = date.hours(); if (this.step == 4) { - hours = hours + '-' + (hours + 4); + hours = hours + "-h" + (hours + 4); } - return hours + 'h' + today(date) + even(date.hours()); + return "h" + hours + today(date) + even(date.hours()); - case 'weekday': - return date.format('dddd').toLowerCase() + - today(date) + currentWeek(date) + even(date.date()); + case "weekday": + return date.format("dddd").toLowerCase() + today(date) + currentWeek(date) + even(date.date()); - case 'day': + case "day": var day = date.date(); - var month = date.format('MMMM').toLowerCase(); - return 'day' + day + ' ' + month + currentMonth(date) + even(day - 1); + var month = date.format("MMMM").toLowerCase(); + return "day" + day + " " + month + currentMonth(date) + even(day - 1); - case 'month': - return date.format('MMMM').toLowerCase() + - currentMonth(date) + even(date.month()); + case "month": + return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); - case 'year': + case "year": var year = date.year(); - return 'year' + year + currentYear(date)+ even(year); + return "year" + year + currentYear(date) + even(year); default: - return ''; + return ""; } }; module.exports = TimeStep; - /***/ }, /* 28 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var stack = __webpack_require__(29); var RangeItem = __webpack_require__(30); @@ -16689,7 +16599,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} data * @param {ItemSet} itemSet */ - function Group (groupId, data, itemSet) { + function Group(groupId, data, itemSet) { this.groupId = groupId; this.subgroups = {}; this.subgroupIndex = 0; @@ -16705,7 +16615,7 @@ return /******/ (function(modules) { // webpackBootstrap }; this.className = null; - this.items = {}; // items filtered by groupId of this group + this.items = {}; // items filtered by groupId of this group this.visibleItems = []; // items currently visible in window this.orderedItems = { byStart: [], @@ -16715,7 +16625,7 @@ return /******/ (function(modules) { // webpackBootstrap var me = this; this.itemSet.body.emitter.on("checkRangedItems", function () { me.checkRangedItems = true; - }) + }); this._create(); @@ -16726,33 +16636,33 @@ return /******/ (function(modules) { // webpackBootstrap * Create DOM elements for the group * @private */ - Group.prototype._create = function() { - var label = document.createElement('div'); - label.className = 'vlabel'; + Group.prototype._create = function () { + var label = document.createElement("div"); + label.className = "vlabel"; this.dom.label = label; - var inner = document.createElement('div'); - inner.className = 'inner'; + var inner = document.createElement("div"); + inner.className = "inner"; label.appendChild(inner); this.dom.inner = inner; - var foreground = document.createElement('div'); - foreground.className = 'group'; - foreground['timeline-group'] = this; + var foreground = document.createElement("div"); + foreground.className = "group"; + foreground["timeline-group"] = this; this.dom.foreground = foreground; - this.dom.background = document.createElement('div'); - this.dom.background.className = 'group'; + this.dom.background = document.createElement("div"); + this.dom.background.className = "group"; - this.dom.axis = document.createElement('div'); - this.dom.axis.className = 'group'; + this.dom.axis = document.createElement("div"); + this.dom.axis.className = "group"; // create a hidden marker to detect when the Timelines container is attached // to the DOM, or the style of a parent of the Timeline is changed from // display:none is changed to visible. - this.dom.marker = document.createElement('div'); - this.dom.marker.style.visibility = 'hidden'; // TODO: ask jos why this is not none? - this.dom.marker.innerHTML = '?'; + this.dom.marker = document.createElement("div"); + this.dom.marker.style.visibility = "hidden"; // TODO: ask jos why this is not none? + this.dom.marker.innerHTML = "?"; this.dom.background.appendChild(this.dom.marker); }; @@ -16760,27 +16670,24 @@ return /******/ (function(modules) { // webpackBootstrap * Set the group data for this group * @param {Object} data Group data, can contain properties content and className */ - Group.prototype.setData = function(data) { + Group.prototype.setData = function (data) { // update contents var content = data && data.content; if (content instanceof Element) { this.dom.inner.appendChild(content); - } - else if (content !== undefined && content !== null) { + } else if (content !== undefined && content !== null) { this.dom.inner.innerHTML = content; - } - else { - this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null + } else { + this.dom.inner.innerHTML = this.groupId || ""; // groupId can be null } // update title - this.dom.label.title = data && data.title || ''; + this.dom.label.title = data && data.title || ""; if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, 'hidden'); - } - else { - util.removeClassName(this.dom.inner, 'hidden'); + util.addClassName(this.dom.inner, "hidden"); + } else { + util.removeClassName(this.dom.inner, "hidden"); } // update className @@ -16814,7 +16721,7 @@ return /******/ (function(modules) { // webpackBootstrap * Get the width of the group label * @return {number} width */ - Group.prototype.getLabelWidth = function() { + Group.prototype.getLabelWidth = function () { return this.props.label.width; }; @@ -16826,7 +16733,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {boolean} [restack=false] Force restacking of all items * @return {boolean} Returns true if the group is resized */ - Group.prototype.redraw = function(range, margin, restack) { + Group.prototype.redraw = function (range, margin, restack) { var resized = false; this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); @@ -16846,10 +16753,11 @@ return /******/ (function(modules) { // webpackBootstrap } // reposition visible items vertically - if (this.itemSet.options.stack) { // TODO: ugly way to access options... + if (this.itemSet.options.stack) { + // TODO: ugly way to access options... stack.stack(this.visibleItems, margin, restack); - } - else { // no stacking + } else { + // no stacking stack.nostack(this.visibleItems, margin, this.subgroups); } @@ -16861,16 +16769,16 @@ return /******/ (function(modules) { // webpackBootstrap this.top = foreground.offsetTop; this.left = foreground.offsetLeft; this.width = foreground.offsetWidth; - resized = util.updateProperty(this, 'height', height) || resized; + resized = util.updateProperty(this, "height", height) || resized; // recalculate size of label - resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized; - resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized; + resized = util.updateProperty(this.props.label, "width", this.dom.inner.clientWidth) || resized; + resized = util.updateProperty(this.props.label, "height", this.dom.inner.clientHeight) || resized; // apply new height - this.dom.background.style.height = height + 'px'; - this.dom.foreground.style.height = height + 'px'; - this.dom.label.style.height = height + 'px'; + this.dom.background.style.height = height + "px"; + this.dom.foreground.style.height = height + "px"; + this.dom.label.style.height = height + "px"; // update vertical position of items after they are re-stacked and the height of the group is calculated for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { @@ -16900,9 +16808,9 @@ return /******/ (function(modules) { // webpackBootstrap var max = visibleItems[0].top + visibleItems[0].height; util.forEach(visibleItems, function (item) { min = Math.min(min, item.top); - max = Math.max(max, (item.top + item.height)); + max = Math.max(max, item.top + item.height); if (item.data.subgroup !== undefined) { - me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height,item.height); + me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); me.subgroups[item.data.subgroup].visible = true; //if (visibleSubgroups.indexOf(item.data.subgroup) == -1){ // visibleSubgroups.push(item.data.subgroup); @@ -16919,8 +16827,7 @@ return /******/ (function(modules) { // webpackBootstrap }); } height = max + margin.item.vertical / 2; - } - else { + } else { height = margin.axis + margin.item.vertical; } height = Math.max(height, this.props.label.height); @@ -16931,7 +16838,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Show this group: attach to the DOM */ - Group.prototype.show = function() { + Group.prototype.show = function () { if (!this.dom.label.parentNode) { this.itemSet.dom.labelSet.appendChild(this.dom.label); } @@ -16952,7 +16859,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide this group: remove from the DOM */ - Group.prototype.hide = function() { + Group.prototype.hide = function () { var label = this.dom.label; if (label.parentNode) { label.parentNode.removeChild(label); @@ -16978,14 +16885,14 @@ return /******/ (function(modules) { // webpackBootstrap * Add an item to the group * @param {Item} item */ - Group.prototype.add = function(item) { + Group.prototype.add = function (item) { this.items[item.id] = item; item.setParent(this); // add to if (item.data.subgroup !== undefined) { if (this.subgroups[item.data.subgroup] === undefined) { - this.subgroups[item.data.subgroup] = {height:0, visible: false, index:this.subgroupIndex, items: []}; + this.subgroups[item.data.subgroup] = { height: 0, visible: false, index: this.subgroupIndex, items: [] }; this.subgroupIndex++; } this.subgroups[item.data.subgroup].items.push(item); @@ -16998,18 +16905,17 @@ return /******/ (function(modules) { // webpackBootstrap } }; - Group.prototype.orderSubgroups = function() { + Group.prototype.orderSubgroups = function () { if (this.subgroupOrderer !== undefined) { var sortArray = []; - if (typeof this.subgroupOrderer == 'string') { + if (typeof this.subgroupOrderer == "string") { for (var subgroup in this.subgroups) { - sortArray.push({subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer]}) + sortArray.push({ subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer] }); } sortArray.sort(function (a, b) { return a.sortField - b.sortField; - }) - } - else if (typeof this.subgroupOrderer == 'function') { + }); + } else if (typeof this.subgroupOrderer == "function") { for (var subgroup in this.subgroups) { sortArray.push(this.subgroups[subgroup].items[0].data); } @@ -17024,7 +16930,7 @@ return /******/ (function(modules) { // webpackBootstrap } }; - Group.prototype.resetSubgroups = function() { + Group.prototype.resetSubgroups = function () { for (var subgroup in this.subgroups) { if (this.subgroups.hasOwnProperty(subgroup)) { this.subgroups[subgroup].visible = false; @@ -17036,7 +16942,7 @@ return /******/ (function(modules) { // webpackBootstrap * Remove an item from the group * @param {Item} item */ - Group.prototype.remove = function(item) { + Group.prototype.remove = function (item) { delete this.items[item.id]; item.setParent(null); @@ -17052,7 +16958,7 @@ return /******/ (function(modules) { // webpackBootstrap * Remove an item from the corresponding DataSet * @param {Item} item */ - Group.prototype.removeFromDataSet = function(item) { + Group.prototype.removeFromDataSet = function (item) { this.itemSet.removeItem(item.id); }; @@ -17060,7 +16966,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Reorder the items */ - Group.prototype.order = function() { + Group.prototype.order = function () { var array = util.toArray(this.items); var startArray = []; var endArray = []; @@ -17089,7 +16995,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Item[]} visibleItems The new visible items. * @private */ - Group.prototype._updateVisibleItems = function(orderedItems, oldVisibleItems, range) { + Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { var visibleItems = []; var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems var interval = (range.end - range.start) / 4; @@ -17099,10 +17005,14 @@ return /******/ (function(modules) { // webpackBootstrap // this function is used to do the binary search. var searchFunction = function (value) { - if (value < lowerBound) {return -1;} - else if (value <= upperBound) {return 0;} - else {return 1;} - } + if (value < lowerBound) { + return -1; + } else if (value <= upperBound) { + return 0; + } else { + return 1; + } + }; // first check if the items that were in view previously are still in view. // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! @@ -17114,11 +17024,11 @@ return /******/ (function(modules) { // webpackBootstrap } // we do a binary search for the items that have only start values. - var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data','start'); + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { - return (item.data.start < lowerBound || item.data.start > upperBound); + return item.data.start < lowerBound || item.data.start > upperBound; }); // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. @@ -17128,14 +17038,13 @@ return /******/ (function(modules) { // webpackBootstrap for (i = 0; i < orderedItems.byEnd.length; i++) { this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); } - } - else { + } else { // we do a binary search for the items that have defined end times. - var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data','end'); + var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, "data", "end"); // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { - return (item.data.end < lowerBound || item.data.end > upperBound); + return item.data.end < lowerBound || item.data.end > upperBound; }); } @@ -17173,8 +17082,7 @@ return /******/ (function(modules) { // webpackBootstrap item = items[i]; if (breakCondition(item)) { break; - } - else { + } else { if (visibleItemsLookup[item.id] === undefined) { visibleItemsLookup[item.id] = true; visibleItems.push(item); @@ -17186,8 +17094,7 @@ return /******/ (function(modules) { // webpackBootstrap item = items[i]; if (breakCondition(item)) { break; - } - else { + } else { if (visibleItemsLookup[item.id] === undefined) { visibleItemsLookup[item.id] = true; visibleItems.push(item); @@ -17195,7 +17102,7 @@ return /******/ (function(modules) { // webpackBootstrap } } } - } + }; /** @@ -17209,16 +17116,15 @@ return /******/ (function(modules) { // webpackBootstrap * @param {{start:number, end:number}} range * @private */ - Group.prototype._checkIfVisible = function(item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - visibleItems.push(item); - } - else { - if (item.displayed) item.hide(); - } + Group.prototype._checkIfVisible = function (item, visibleItems, range) { + if (item.isVisible(range)) { + if (!item.displayed) item.show(); + // reposition item horizontally + item.repositionX(); + visibleItems.push(item); + } else { + if (item.displayed) item.hide(); + } }; @@ -17233,14 +17139,13 @@ return /******/ (function(modules) { // webpackBootstrap * @param {{start:number, end:number}} range * @private */ - Group.prototype._checkIfVisibleWithReference = function(item, visibleItems, visibleItemsLookup, range) { + Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { if (item.isVisible(range)) { if (visibleItemsLookup[item.id] === undefined) { visibleItemsLookup[item.id] = true; visibleItems.push(item); } - } - else { + } else { if (item.displayed) item.hide(); } }; @@ -17249,11 +17154,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Group; - /***/ }, /* 29 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // Utility functions for ordering and stacking of items var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors @@ -17261,7 +17167,7 @@ return /******/ (function(modules) { // webpackBootstrap * Order items by their start data * @param {Item[]} items */ - exports.orderByStart = function(items) { + exports.orderByStart = function (items) { items.sort(function (a, b) { return a.data.start - b.data.start; }); @@ -17272,10 +17178,10 @@ return /******/ (function(modules) { // webpackBootstrap * is used. * @param {Item[]} items */ - exports.orderByEnd = function(items) { + exports.orderByEnd = function (items) { items.sort(function (a, b) { - var aTime = ('end' in a.data) ? a.data.end : a.data.start, - bTime = ('end' in b.data) ? b.data.end : b.data.start; + var aTime = "end" in a.data ? a.data.end : a.data.start, + bTime = "end" in b.data ? b.data.end : b.data.start; return aTime - bTime; }); @@ -17292,7 +17198,7 @@ return /******/ (function(modules) { // webpackBootstrap * If true, all items will be repositioned. If false (default), only * items having a top===null will be re-stacked */ - exports.stack = function(items, margin, force) { + exports.stack = function (items, margin, force) { var i, iMax; if (force) { @@ -17338,7 +17244,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. */ - exports.nostack = function(items, margin, subgroups) { + exports.nostack = function (items, margin, subgroups) { var i, iMax, newTop; // reset top position of all items @@ -17353,8 +17259,7 @@ return /******/ (function(modules) { // webpackBootstrap } } items[i].top = newTop; - } - else { + } else { items[i].top = margin.axis; } } @@ -17370,18 +17275,16 @@ return /******/ (function(modules) { // webpackBootstrap * minimum required margin. * @return {boolean} true if a and b collide, else false */ - exports.collision = function(a, b, margin) { - return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) && - (a.left + a.width + margin.horizontal - EPSILON) > b.left && - (a.top - margin.vertical + EPSILON) < (b.top + b.height) && - (a.top + a.height + margin.vertical - EPSILON) > b.top); + exports.collision = function (a, b, margin) { + return a.left - margin.horizontal + EPSILON < b.left + b.width && a.left + a.width + margin.horizontal - EPSILON > b.left && a.top - margin.vertical + EPSILON < b.top + b.height && a.top + a.height + margin.vertical - EPSILON > b.top; }; - /***/ }, /* 30 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Hammer = __webpack_require__(19); var Item = __webpack_require__(31); @@ -17395,7 +17298,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [options] Configuration options * // TODO: describe options */ - function RangeItem (data, conversion, options) { + function RangeItem(data, conversion, options) { this.props = { content: { width: 0 @@ -17406,63 +17309,63 @@ return /******/ (function(modules) { // webpackBootstrap // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data.id); + throw new Error("Property \"start\" missing in item " + data.id); } if (data.end == undefined) { - throw new Error('Property "end" missing in item ' + data.id); + throw new Error("Property \"end\" missing in item " + data.id); } } Item.call(this, data, conversion, options); } - RangeItem.prototype = new Item (null, null, null); + RangeItem.prototype = new Item(null, null, null); - RangeItem.prototype.baseClassName = 'item range'; + RangeItem.prototype.baseClassName = "item range"; /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - RangeItem.prototype.isVisible = function(range) { + RangeItem.prototype.isVisible = function (range) { // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); + return this.data.start < range.end && this.data.end > range.start; }; /** * Repaint the item */ - RangeItem.prototype.redraw = function() { + RangeItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // background box - dom.box = document.createElement('div'); + // background box + dom.box = document.createElement("div"); // className is updated in redraw() // contents box - dom.content = document.createElement('div'); - dom.content.className = 'content'; + dom.content = document.createElement("div"); + dom.content.className = "content"; dom.box.appendChild(dom.content); // attach this item as attribute - dom.box['timeline-item'] = this; + dom.box["timeline-item"] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); + throw new Error("Cannot redraw item: no parent attached"); } if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { - throw new Error('Cannot redraw item: parent has no foreground container element'); + throw new Error("Cannot redraw item: parent has no foreground container element"); } foreground.appendChild(dom.box); } @@ -17479,20 +17382,19 @@ return /******/ (function(modules) { // webpackBootstrap this._updateStyle(this.dom.box); // update class - var className = (this.data.className ? (' ' + this.data.className) : '') + - (this.selected ? ' selected' : ''); + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); dom.box.className = this.baseClassName + className; // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; // recalculate size // turn off max-width to be able to calculate the real width // this causes an extra browser repaint/reflow, but so be it - this.dom.content.style.maxWidth = 'none'; + this.dom.content.style.maxWidth = "none"; this.props.content.width = this.dom.content.offsetWidth; this.height = this.dom.box.offsetHeight; - this.dom.content.style.maxWidth = ''; + this.dom.content.style.maxWidth = ""; this.dirty = false; } @@ -17506,7 +17408,7 @@ return /******/ (function(modules) { // webpackBootstrap * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ - RangeItem.prototype.show = function() { + RangeItem.prototype.show = function () { if (!this.displayed) { this.redraw(); } @@ -17516,7 +17418,7 @@ return /******/ (function(modules) { // webpackBootstrap * Hide the item from the DOM (when visible) * @return {Boolean} changed */ - RangeItem.prototype.hide = function() { + RangeItem.prototype.hide = function () { if (this.displayed) { var box = this.dom.box; @@ -17535,7 +17437,7 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - RangeItem.prototype.repositionX = function() { + RangeItem.prototype.repositionX = function () { var parentWidth = this.parent.width; var start = this.conversion.toScreen(this.data.start); var end = this.conversion.toScreen(this.data.end); @@ -17559,50 +17461,46 @@ return /******/ (function(modules) { // webpackBootstrap // Note: The calculation of width is an optimistic calculation, giving // a width which will not change when moving the Timeline // So no re-stacking needed, which is nicer for the eye; - } - else { + } else { this.left = start; this.width = boxWidth; contentWidth = Math.min(end - start - 2 * this.options.padding, this.props.content.width); } - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; + this.dom.box.style.left = this.left + "px"; + this.dom.box.style.width = boxWidth + "px"; switch (this.options.align) { - case 'left': - this.dom.content.style.left = '0'; + case "left": + this.dom.content.style.left = "0"; break; - case 'right': - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px'; + case "right": + this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; break; - case 'center': - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px'; + case "center": + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; break; - default: // 'auto' + default: + // 'auto' // when range exceeds left of the window, position the contents at the left of the visible area if (this.overflow) { if (end > 0) { contentLeft = Math.max(-start, 0); - } - else { + } else { contentLeft = -contentWidth; // ensure it's not visible anymore } - } - else { + } else { if (start < 0) { - contentLeft = Math.min(-start, - (end - start - contentWidth - 2 * this.options.padding)); + contentLeft = Math.min(-start, end - start - contentWidth - 2 * this.options.padding); // TODO: remove the need for options.padding. it's terrible. - } - else { + } else { contentLeft = 0; } } - this.dom.content.style.left = contentLeft + 'px'; + this.dom.content.style.left = contentLeft + "px"; } }; @@ -17610,15 +17508,14 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item vertically * @Override */ - RangeItem.prototype.repositionY = function() { + RangeItem.prototype.repositionY = function () { var orientation = this.options.orientation, box = this.dom.box; - if (orientation == 'top') { - box.style.top = this.top + 'px'; - } - else { - box.style.top = (this.parent.height - this.top - this.height) + 'px'; + if (orientation == "top") { + box.style.top = this.top + "px"; + } else { + box.style.top = this.parent.height - this.top - this.height + "px"; } }; @@ -17629,21 +17526,18 @@ return /******/ (function(modules) { // webpackBootstrap RangeItem.prototype._repaintDragLeft = function () { if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { // create and show drag area - var dragLeft = document.createElement('div'); - dragLeft.className = 'drag-left'; + var dragLeft = document.createElement("div"); + dragLeft.className = "drag-left"; dragLeft.dragLeftItem = this; // TODO: this should be redundant? Hammer(dragLeft, { preventDefault: true - }).on('drag', function () { - //console.log('drag left') - }); + }).on("drag", function () {}); this.dom.box.appendChild(dragLeft); this.dom.dragLeft = dragLeft; - } - else if (!this.selected && this.dom.dragLeft) { + } else if (!this.selected && this.dom.dragLeft) { // delete drag area if (this.dom.dragLeft.parentNode) { this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); @@ -17659,21 +17553,18 @@ return /******/ (function(modules) { // webpackBootstrap RangeItem.prototype._repaintDragRight = function () { if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { // create and show drag area - var dragRight = document.createElement('div'); - dragRight.className = 'drag-right'; + var dragRight = document.createElement("div"); + dragRight.className = "drag-right"; dragRight.dragRightItem = this; // TODO: this should be redundant? Hammer(dragRight, { preventDefault: true - }).on('drag', function () { - //console.log('drag right') - }); + }).on("drag", function () {}); this.dom.box.appendChild(dragRight); this.dom.dragRight = dragRight; - } - else if (!this.selected && this.dom.dragRight) { + } else if (!this.selected && this.dom.dragRight) { // delete drag area if (this.dom.dragRight.parentNode) { this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); @@ -17683,12 +17574,15 @@ return /******/ (function(modules) { // webpackBootstrap }; module.exports = RangeItem; - + //console.log('drag left') + //console.log('drag right') /***/ }, /* 31 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Hammer = __webpack_require__(19); var util = __webpack_require__(1); @@ -17701,7 +17595,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} options Configuration options * // TODO: describe available options */ - function Item (data, conversion, options) { + function Item(data, conversion, options) { this.id = null; this.parent = null; this.data = data; @@ -17724,7 +17618,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Select current item */ - Item.prototype.select = function() { + Item.prototype.select = function () { this.selected = true; this.dirty = true; if (this.displayed) this.redraw(); @@ -17733,7 +17627,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Unselect current item */ - Item.prototype.unselect = function() { + Item.prototype.unselect = function () { this.selected = false; this.dirty = true; if (this.displayed) this.redraw(); @@ -17744,7 +17638,7 @@ return /******/ (function(modules) { // webpackBootstrap * be changed. When the item is displayed, it will be redrawn immediately. * @param {Object} data */ - Item.prototype.setData = function(data) { + Item.prototype.setData = function (data) { this.data = data; this.dirty = true; if (this.displayed) this.redraw(); @@ -17754,15 +17648,14 @@ return /******/ (function(modules) { // webpackBootstrap * Set a parent for the item * @param {ItemSet | Group} parent */ - Item.prototype.setParent = function(parent) { + Item.prototype.setParent = function (parent) { if (this.displayed) { this.hide(); this.parent = parent; if (this.parent) { this.show(); } - } - else { + } else { this.parent = parent; } }; @@ -17772,7 +17665,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - Item.prototype.isVisible = function(range) { + Item.prototype.isVisible = function (range) { // Should be implemented by Item implementations return false; }; @@ -17781,7 +17674,7 @@ return /******/ (function(modules) { // webpackBootstrap * Show the Item in the DOM (when not already visible) * @return {Boolean} changed */ - Item.prototype.show = function() { + Item.prototype.show = function () { return false; }; @@ -17789,30 +17682,24 @@ return /******/ (function(modules) { // webpackBootstrap * Hide the Item from the DOM (when visible) * @return {Boolean} changed */ - Item.prototype.hide = function() { + Item.prototype.hide = function () { return false; }; /** * Repaint the item */ - Item.prototype.redraw = function() { - // should be implemented by the item - }; + Item.prototype.redraw = function () {}; /** * Reposition the Item horizontally */ - Item.prototype.repositionX = function() { - // should be implemented by the item - }; + Item.prototype.repositionX = function () {}; /** * Reposition the Item vertically */ - Item.prototype.repositionY = function() { - // should be implemented by the item - }; + Item.prototype.repositionY = function () {}; /** * Repaint a delete button on the top right of the item when the item is selected @@ -17824,21 +17711,20 @@ return /******/ (function(modules) { // webpackBootstrap // create and show button var me = this; - var deleteButton = document.createElement('div'); - deleteButton.className = 'delete'; - deleteButton.title = 'Delete this item'; + var deleteButton = document.createElement("div"); + deleteButton.className = "delete"; + deleteButton.title = "Delete this item"; Hammer(deleteButton, { preventDefault: true - }).on('tap', function (event) { + }).on("tap", function (event) { me.parent.removeFromDataSet(me); event.stopPropagation(); }); anchor.appendChild(deleteButton); this.dom.deleteButton = deleteButton; - } - else if (!this.selected && this.dom.deleteButton) { + } else if (!this.selected && this.dom.deleteButton) { // remove button if (this.dom.deleteButton.parentNode) { this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); @@ -17857,23 +17743,20 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.template) { var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset content = this.options.template(itemData); - } - else { + } else { content = this.data.content; } - if(content !== this.content) { + if (content !== this.content) { // only replace the content when changed if (content instanceof Element) { - element.innerHTML = ''; + element.innerHTML = ""; element.appendChild(content); - } - else if (content != undefined) { + } else if (content != undefined) { element.innerHTML = content; - } - else { - if (!(this.data.type == 'background' && this.data.content === undefined)) { - throw new Error('Property "content" missing in item ' + this.id); + } else { + if (!(this.data.type == "background" && this.data.content === undefined)) { + throw new Error("Property \"content\" missing in item " + this.id); } } @@ -17888,10 +17771,9 @@ return /******/ (function(modules) { // webpackBootstrap */ Item.prototype._updateTitle = function (element) { if (this.data.title != null) { - element.title = this.data.title || ''; - } - else { - element.removeAttribute('title'); + element.title = this.data.title || ""; + } else { + element.removeAttribute("title"); } }; @@ -17900,17 +17782,15 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Element} element HTML element to which the attributes will be attached * @private */ - Item.prototype._updateDataAttributes = function(element) { + Item.prototype._updateDataAttributes = function (element) { if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { var attributes = []; if (Array.isArray(this.options.dataAttributes)) { attributes = this.options.dataAttributes; - } - else if (this.options.dataAttributes == 'all') { + } else if (this.options.dataAttributes == "all") { attributes = Object.keys(this.data); - } - else { + } else { return; } @@ -17919,10 +17799,9 @@ return /******/ (function(modules) { // webpackBootstrap var value = this.data[name]; if (value != null) { - element.setAttribute('data-' + name, value); - } - else { - element.removeAttribute('data-' + name); + element.setAttribute("data-" + name, value); + } else { + element.removeAttribute("data-" + name); } } } @@ -17933,7 +17812,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param element * @private */ - Item.prototype._updateStyle = function(element) { + Item.prototype._updateStyle = function (element) { // remove old styles if (this.style) { util.removeCssText(element, this.style); @@ -17948,12 +17827,16 @@ return /******/ (function(modules) { // webpackBootstrap }; module.exports = Item; - + // should be implemented by the item + // should be implemented by the item + // should be implemented by the item /***/ }, /* 32 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var Group = __webpack_require__(28); @@ -17963,7 +17846,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} data * @param {ItemSet} itemSet */ - function BackgroundGroup (groupId, data, itemSet) { + function BackgroundGroup(groupId, data, itemSet) { Group.call(this, groupId, data, itemSet); this.width = 0; @@ -17981,7 +17864,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {boolean} [restack=false] Force restacking of all items * @return {boolean} Returns true if the group is resized */ - BackgroundGroup.prototype.redraw = function(range, margin, restack) { + BackgroundGroup.prototype.redraw = function (range, margin, restack) { var resized = false; this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); @@ -17990,7 +17873,7 @@ return /******/ (function(modules) { // webpackBootstrap this.width = this.dom.background.offsetWidth; // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = '0'; + this.dom.background.style.height = "0"; // update vertical position of items after they are re-stacked and the height of the group is calculated for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { @@ -18004,7 +17887,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Show this group: attach to the DOM */ - BackgroundGroup.prototype.show = function() { + BackgroundGroup.prototype.show = function () { if (!this.dom.background.parentNode) { this.itemSet.dom.background.appendChild(this.dom.background); } @@ -18012,11 +17895,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BackgroundGroup; - /***/ }, /* 33 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Item = __webpack_require__(31); var util = __webpack_require__(1); @@ -18030,7 +17914,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [options] Configuration options * // TODO: describe available options */ - function BoxItem (data, conversion, options) { + function BoxItem(data, conversion, options) { this.props = { dot: { width: 0, @@ -18045,31 +17929,31 @@ return /******/ (function(modules) { // webpackBootstrap // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + throw new Error("Property \"start\" missing in item " + data); } } Item.call(this, data, conversion, options); } - BoxItem.prototype = new Item (null, null, null); + BoxItem.prototype = new Item(null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - BoxItem.prototype.isVisible = function(range) { + BoxItem.prototype.isVisible = function (range) { // determine visibility // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + return this.data.start > range.start - interval && this.data.start < range.end + interval; }; /** * Repaint the item */ - BoxItem.prototype.redraw = function() { + BoxItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM @@ -18077,44 +17961,44 @@ return /******/ (function(modules) { // webpackBootstrap dom = this.dom; // create main box - dom.box = document.createElement('DIV'); + dom.box = document.createElement("DIV"); // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'content'; + dom.content = document.createElement("DIV"); + dom.content.className = "content"; dom.box.appendChild(dom.content); // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; + dom.line = document.createElement("DIV"); + dom.line.className = "line"; // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; + dom.dot = document.createElement("DIV"); + dom.dot.className = "dot"; // attach this item as attribute - dom.box['timeline-item'] = this; + dom.box["timeline-item"] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); + throw new Error("Cannot redraw item: no parent attached"); } if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; - if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); + if (!foreground) throw new Error("Cannot redraw item: parent has no foreground container element"); foreground.appendChild(dom.box); } if (!dom.line.parentNode) { var background = this.parent.dom.background; - if (!background) throw new Error('Cannot redraw item: parent has no background container element'); + if (!background) throw new Error("Cannot redraw item: parent has no background container element"); background.appendChild(dom.line); } if (!dom.dot.parentNode) { var axis = this.parent.dom.axis; - if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); + if (!background) throw new Error("Cannot redraw item: parent has no axis container element"); axis.appendChild(dom.dot); } this.displayed = true; @@ -18130,11 +18014,10 @@ return /******/ (function(modules) { // webpackBootstrap this._updateStyle(this.dom.box); // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; - dom.dot.className = 'item dot' + className; + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = "item box" + className; + dom.line.className = "item line" + className; + dom.dot.className = "item dot" + className; // recalculate size this.props.dot.height = dom.dot.offsetHeight; @@ -18153,7 +18036,7 @@ return /******/ (function(modules) { // webpackBootstrap * Show the item in the DOM (when not already displayed). The items DOM will * be created when needed. */ - BoxItem.prototype.show = function() { + BoxItem.prototype.show = function () { if (!this.displayed) { this.redraw(); } @@ -18162,13 +18045,13 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) */ - BoxItem.prototype.hide = function() { + BoxItem.prototype.hide = function () { if (this.displayed) { var dom = this.dom; - if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); - if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); - if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); + if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); + if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); + if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); this.top = null; this.left = null; @@ -18181,7 +18064,7 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - BoxItem.prototype.repositionX = function() { + BoxItem.prototype.repositionX = function () { var start = this.conversion.toScreen(this.data.start); var align = this.options.align; var left; @@ -18190,63 +18073,62 @@ return /******/ (function(modules) { // webpackBootstrap var dot = this.dom.dot; // calculate left position of the box - if (align == 'right') { + if (align == "right") { this.left = start - this.width; - } - else if (align == 'left') { + } else if (align == "left") { this.left = start; - } - else { + } else { // default or 'center' this.left = start - this.width / 2; } // reposition box - box.style.left = this.left + 'px'; + box.style.left = this.left + "px"; // reposition line - line.style.left = (start - this.props.line.width / 2) + 'px'; + line.style.left = start - this.props.line.width / 2 + "px"; // reposition dot - dot.style.left = (start - this.props.dot.width / 2) + 'px'; + dot.style.left = start - this.props.dot.width / 2 + "px"; }; /** * Reposition the item vertically * @Override */ - BoxItem.prototype.repositionY = function() { + BoxItem.prototype.repositionY = function () { var orientation = this.options.orientation; var box = this.dom.box; var line = this.dom.line; var dot = this.dom.dot; - if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; + if (orientation == "top") { + box.style.top = (this.top || 0) + "px"; - line.style.top = '0'; - line.style.height = (this.parent.top + this.top + 1) + 'px'; - line.style.bottom = ''; - } - else { // orientation 'bottom' + line.style.top = "0"; + line.style.height = this.parent.top + this.top + 1 + "px"; + line.style.bottom = ""; + } else { + // orientation 'bottom' var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; - box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; - line.style.top = (itemSetHeight - lineHeight) + 'px'; - line.style.bottom = '0'; + box.style.top = (this.parent.height - this.top - this.height || 0) + "px"; + line.style.top = itemSetHeight - lineHeight + "px"; + line.style.bottom = "0"; } - dot.style.top = (-this.props.dot.height / 2) + 'px'; + dot.style.top = -this.props.dot.height / 2 + "px"; }; module.exports = BoxItem; - /***/ }, /* 34 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Item = __webpack_require__(31); /** @@ -18259,7 +18141,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [options] Configuration options * // TODO: describe available options */ - function PointItem (data, conversion, options) { + function PointItem(data, conversion, options) { this.props = { dot: { top: 0, @@ -18275,31 +18157,31 @@ return /******/ (function(modules) { // webpackBootstrap // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + throw new Error("Property \"start\" missing in item " + data); } } Item.call(this, data, conversion, options); } - PointItem.prototype = new Item (null, null, null); + PointItem.prototype = new Item(null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - PointItem.prototype.isVisible = function(range) { + PointItem.prototype.isVisible = function (range) { // determine visibility // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + return this.data.start > range.start - interval && this.data.start < range.end + interval; }; /** * Repaint the item */ - PointItem.prototype.redraw = function() { + PointItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM @@ -18307,32 +18189,32 @@ return /******/ (function(modules) { // webpackBootstrap dom = this.dom; // background box - dom.point = document.createElement('div'); + dom.point = document.createElement("div"); // className is updated in redraw() // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'content'; + dom.content = document.createElement("div"); + dom.content.className = "content"; dom.point.appendChild(dom.content); // dot at start - dom.dot = document.createElement('div'); + dom.dot = document.createElement("div"); dom.point.appendChild(dom.dot); // attach this item as attribute - dom.point['timeline-item'] = this; + dom.point["timeline-item"] = this; this.dirty = true; } // append DOM to parent DOM if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); + throw new Error("Cannot redraw item: no parent attached"); } if (!dom.point.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { - throw new Error('Cannot redraw item: parent has no foreground container element'); + throw new Error("Cannot redraw item: parent has no foreground container element"); } foreground.appendChild(dom.point); } @@ -18349,10 +18231,9 @@ return /******/ (function(modules) { // webpackBootstrap this._updateStyle(this.dom.point); // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - dom.point.className = 'item point' + className; - dom.dot.className = 'item dot' + className; + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.point.className = "item point" + className; + dom.dot.className = "item dot" + className; // recalculate size this.width = dom.point.offsetWidth; @@ -18362,11 +18243,11 @@ return /******/ (function(modules) { // webpackBootstrap this.props.content.height = dom.content.offsetHeight; // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; - dom.dot.style.left = (this.props.dot.width / 2) + 'px'; + dom.dot.style.top = (this.height - this.props.dot.height) / 2 + "px"; + dom.dot.style.left = this.props.dot.width / 2 + "px"; this.dirty = false; } @@ -18378,7 +18259,7 @@ return /******/ (function(modules) { // webpackBootstrap * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ - PointItem.prototype.show = function() { + PointItem.prototype.show = function () { if (!this.displayed) { this.redraw(); } @@ -18387,7 +18268,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) */ - PointItem.prototype.hide = function() { + PointItem.prototype.hide = function () { if (this.displayed) { if (this.dom.point.parentNode) { this.dom.point.parentNode.removeChild(this.dom.point); @@ -18404,38 +18285,38 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - PointItem.prototype.repositionX = function() { + PointItem.prototype.repositionX = function () { var start = this.conversion.toScreen(this.data.start); this.left = start - this.props.dot.width; // reposition point - this.dom.point.style.left = this.left + 'px'; + this.dom.point.style.left = this.left + "px"; }; /** * Reposition the item vertically * @Override */ - PointItem.prototype.repositionY = function() { + PointItem.prototype.repositionY = function () { var orientation = this.options.orientation, point = this.dom.point; - if (orientation == 'top') { - point.style.top = this.top + 'px'; - } - else { - point.style.top = (this.parent.height - this.top - this.height) + 'px'; + if (orientation == "top") { + point.style.top = this.top + "px"; + } else { + point.style.top = this.parent.height - this.top - this.height + "px"; } }; module.exports = PointItem; - /***/ }, /* 35 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Hammer = __webpack_require__(19); var Item = __webpack_require__(31); var BackgroundGroup = __webpack_require__(32); @@ -18452,7 +18333,7 @@ return /******/ (function(modules) { // webpackBootstrap * // TODO: describe options */ // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation - function BackgroundItem (data, conversion, options) { + function BackgroundItem(data, conversion, options) { this.props = { content: { width: 0 @@ -18463,10 +18344,10 @@ return /******/ (function(modules) { // webpackBootstrap // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data.id); + throw new Error("Property \"start\" missing in item " + data.id); } if (data.end == undefined) { - throw new Error('Property "end" missing in item ' + data.id); + throw new Error("Property \"end\" missing in item " + data.id); } } @@ -18475,9 +18356,9 @@ return /******/ (function(modules) { // webpackBootstrap this.emptyContent = false; } - BackgroundItem.prototype = new Item (null, null, null); + BackgroundItem.prototype = new Item(null, null, null); - BackgroundItem.prototype.baseClassName = 'item background'; + BackgroundItem.prototype.baseClassName = "item background"; BackgroundItem.prototype.stack = false; /** @@ -18485,15 +18366,15 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - BackgroundItem.prototype.isVisible = function(range) { + BackgroundItem.prototype.isVisible = function (range) { // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); + return this.data.start < range.end && this.data.end > range.start; }; /** * Repaint the item */ - BackgroundItem.prototype.redraw = function() { + BackgroundItem.prototype.redraw = function () { var dom = this.dom; if (!dom) { // create DOM @@ -18501,12 +18382,12 @@ return /******/ (function(modules) { // webpackBootstrap dom = this.dom; // background box - dom.box = document.createElement('div'); + dom.box = document.createElement("div"); // className is updated in redraw() // contents box - dom.content = document.createElement('div'); - dom.content.className = 'content'; + dom.content = document.createElement("div"); + dom.content.className = "content"; dom.box.appendChild(dom.content); // Note: we do NOT attach this item as attribute to the DOM, @@ -18518,12 +18399,12 @@ return /******/ (function(modules) { // webpackBootstrap // append DOM to parent DOM if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); + throw new Error("Cannot redraw item: no parent attached"); } if (!dom.box.parentNode) { var background = this.parent.dom.background; if (!background) { - throw new Error('Cannot redraw item: parent has no background container element'); + throw new Error("Cannot redraw item: parent has no background container element"); } background.appendChild(dom.box); } @@ -18540,12 +18421,11 @@ return /******/ (function(modules) { // webpackBootstrap this._updateStyle(this.dom.box); // update class - var className = (this.data.className ? (' ' + this.data.className) : '') + - (this.selected ? ' selected' : ''); + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); dom.box.className = this.baseClassName + className; // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; // recalculate size this.props.content.width = this.dom.content.offsetWidth; @@ -18577,10 +18457,10 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item vertically * @Override */ - BackgroundItem.prototype.repositionY = function(margin) { - var onTop = this.options.orientation === 'top'; - this.dom.content.style.top = onTop ? '' : '0'; - this.dom.content.style.bottom = onTop ? '0' : ''; + BackgroundItem.prototype.repositionY = function (margin) { + var onTop = this.options.orientation === "top"; + this.dom.content.style.top = onTop ? "" : "0"; + this.dom.content.style.bottom = onTop ? "0" : ""; var height; // special positioning for subgroups @@ -18592,7 +18472,7 @@ return /******/ (function(modules) { // webpackBootstrap if (onTop == true) { // the first subgroup will have to account for the distance from the top to the first item. height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - height += subgroupIndex == 0 ? margin.axis - 0.5*margin.item.vertical : 0; + height += subgroupIndex == 0 ? margin.axis - 0.5 * margin.item.vertical : 0; var newTop = this.parent.top; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { @@ -18604,8 +18484,8 @@ return /******/ (function(modules) { // webpackBootstrap // the others will have to be offset downwards with this same distance. newTop += subgroupIndex != 0 ? margin.axis - 0.5 * margin.item.vertical : 0; - this.dom.box.style.top = newTop + 'px'; - this.dom.box.style.bottom = ''; + this.dom.box.style.top = newTop + "px"; + this.dom.box.style.bottom = ""; } // and when the orientation is bottom: else { @@ -18618,8 +18498,8 @@ return /******/ (function(modules) { // webpackBootstrap } } height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - this.dom.box.style.top = newTop + 'px'; - this.dom.box.style.bottom = ''; + this.dom.box.style.top = newTop + "px"; + this.dom.box.style.bottom = ""; } } // and in the case of no subgroups: @@ -18627,29 +18507,27 @@ return /******/ (function(modules) { // webpackBootstrap // we want backgrounds with groups to only show in groups. if (this.parent instanceof BackgroundGroup) { // if the item is not in a group: - height = Math.max(this.parent.height, - this.parent.itemSet.body.domProps.center.height, - this.parent.itemSet.body.domProps.centerContainer.height); - this.dom.box.style.top = onTop ? '0' : ''; - this.dom.box.style.bottom = onTop ? '' : '0'; - } - else { + height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); + this.dom.box.style.top = onTop ? "0" : ""; + this.dom.box.style.bottom = onTop ? "" : "0"; + } else { height = this.parent.height; // same alignment for items when orientation is top or bottom - this.dom.box.style.top = this.parent.top + 'px'; - this.dom.box.style.bottom = ''; + this.dom.box.style.top = this.parent.top + "px"; + this.dom.box.style.bottom = ""; } } - this.dom.box.style.height = height + 'px'; + this.dom.box.style.height = height + "px"; }; module.exports = BackgroundItem; - /***/ }, /* 36 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var keycharm = __webpack_require__(37); var Emitter = __webpack_require__(11); var Hammer = __webpack_require__(19); @@ -18672,21 +18550,17 @@ return /******/ (function(modules) { // webpackBootstrap container: container }; - this.dom.overlay = document.createElement('div'); - this.dom.overlay.className = 'overlay'; + this.dom.overlay = document.createElement("div"); + this.dom.overlay.className = "overlay"; this.dom.container.appendChild(this.dom.overlay); - this.hammer = Hammer(this.dom.overlay, {prevent_default: false}); - this.hammer.on('tap', this._onTapOverlay.bind(this)); + this.hammer = Hammer(this.dom.overlay, { prevent_default: false }); + this.hammer.on("tap", this._onTapOverlay.bind(this)); // block all touch events (except tap) var me = this; - var events = [ - 'touch', 'pinch', - 'doubletap', 'hold', - 'dragstart', 'drag', 'dragend', - 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + var events = ["touch", "pinch", "doubletap", "hold", "dragstart", "drag", "dragend", "mousewheel", "DOMMouseScroll" // DOMMouseScroll is needed for Firefox ]; events.forEach(function (event) { me.hammer.on(event, function (event) { @@ -18695,8 +18569,8 @@ return /******/ (function(modules) { // webpackBootstrap }); // attach a tap event to the window, in order to deactivate when clicking outside the timeline - this.windowHammer = Hammer(window, {prevent_default: false}); - this.windowHammer.on('tap', function (event) { + this.windowHammer = Hammer(window, { prevent_default: false }); + this.windowHammer.on("tap", function (event) { // deactivate when clicked outside the container if (!_hasParent(event.target, container)) { me.deactivate(); @@ -18745,15 +18619,15 @@ return /******/ (function(modules) { // webpackBootstrap Activator.current = this; this.active = true; - this.dom.overlay.style.display = 'none'; - util.addClassName(this.dom.container, 'vis-active'); + this.dom.overlay.style.display = "none"; + util.addClassName(this.dom.container, "vis-active"); - this.emit('change'); - this.emit('activate'); + this.emit("change"); + this.emit("activate"); // ugly hack: bind ESC after emitting the events, as the Network rebinds all // keyboard events on a 'change' event - this.keycharm.bind('esc', this.escListener); + this.keycharm.bind("esc", this.escListener); }; /** @@ -18762,12 +18636,12 @@ return /******/ (function(modules) { // webpackBootstrap */ Activator.prototype.deactivate = function () { this.active = false; - this.dom.overlay.style.display = ''; - util.removeClassName(this.dom.container, 'vis-active'); - this.keycharm.unbind('esc', this.escListener); + this.dom.overlay.style.display = ""; + util.removeClassName(this.dom.container, "vis-active"); + this.keycharm.unbind("esc", this.escListener); - this.emit('change'); - this.emit('deactivate'); + this.emit("change"); + this.emit("deactivate"); }; /** @@ -18793,7 +18667,7 @@ return /******/ (function(modules) { // webpackBootstrap function _hasParent(element, parent) { while (element) { if (element === parent) { - return true + return true; } element = element.parentNode; } @@ -18802,7 +18676,6 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Activator; - /***/ }, /* 37 */ /***/ function(module, exports, __webpack_require__) { @@ -19005,6 +18878,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 38 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Hammer = __webpack_require__(19); var util = __webpack_require__(1); var Component = __webpack_require__(23); @@ -19020,14 +18895,14 @@ return /******/ (function(modules) { // webpackBootstrap * @extends Component */ - function CustomTime (body, options) { + function CustomTime(body, options) { this.body = body; // default options this.defaultOptions = { showCustomTime: false, locales: locales, - locale: 'en', + locale: "en", id: 0 }; this.options = util.extend({}, this.defaultOptions); @@ -19035,9 +18910,9 @@ return /******/ (function(modules) { // webpackBootstrap if (options && options.time) { this.customTime = options.time; } else { - this.customTime = new Date(); + this.customTime = new Date(); } - + this.eventParams = {}; // stores state parameters while dragging the bar // create the DOM @@ -19053,10 +18928,10 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} options Available parameters: * {boolean} [showCustomTime] */ - CustomTime.prototype.setOptions = function(options) { + CustomTime.prototype.setOptions = function (options) { if (options) { // copy all options that we know - util.selectiveExtend(['showCustomTime', 'locale', 'locales', 'id'], this.options, options); + util.selectiveExtend(["showCustomTime", "locale", "locales", "id"], this.options, options); // Triggered by addCustomTimeBar, redraw to add new bar if (this.options.id) { @@ -19069,29 +18944,29 @@ return /******/ (function(modules) { // webpackBootstrap * Create the DOM for the custom time * @private */ - CustomTime.prototype._create = function() { - var bar = document.createElement('div'); - bar.className = 'customtime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + CustomTime.prototype._create = function () { + var bar = document.createElement("div"); + bar.className = "customtime"; + bar.style.position = "absolute"; + bar.style.top = "0px"; + bar.style.height = "100%"; this.bar = bar; - var drag = document.createElement('div'); - drag.style.position = 'relative'; - drag.style.top = '0px'; - drag.style.left = '-10px'; - drag.style.height = '100%'; - drag.style.width = '20px'; + var drag = document.createElement("div"); + drag.style.position = "relative"; + drag.style.top = "0px"; + drag.style.left = "-10px"; + drag.style.height = "100%"; + drag.style.width = "20px"; bar.appendChild(drag); // attach event listeners this.hammer = Hammer(bar, { prevent_default: true }); - this.hammer.on('dragstart', this._onDragStart.bind(this)); - this.hammer.on('drag', this._onDrag.bind(this)); - this.hammer.on('dragend', this._onDragEnd.bind(this)); + this.hammer.on("dragstart", this._onDragStart.bind(this)); + this.hammer.on("drag", this._onDrag.bind(this)); + this.hammer.on("dragend", this._onDragEnd.bind(this)); }; /** @@ -19125,13 +19000,12 @@ return /******/ (function(modules) { // webpackBootstrap var x = this.body.util.toScreen(this.customTime); var locale = this.options.locales[this.options.locale]; - var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); + var title = locale.time + ": " + moment(this.customTime).format("dddd, MMMM Do YYYY, H:mm:ss"); title = title.charAt(0).toUpperCase() + title.substring(1); - this.bar.style.left = x + 'px'; + this.bar.style.left = x + "px"; this.bar.title = title; - } - else { + } else { // remove the line from the DOM if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); @@ -19145,8 +19019,8 @@ return /******/ (function(modules) { // webpackBootstrap * Set custom time. * @param {Date | number | string} time */ - CustomTime.prototype.setCustomTime = function(time) { - this.customTime = util.convert(time, 'Date'); + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, "Date"); this.redraw(); }; @@ -19154,7 +19028,7 @@ return /******/ (function(modules) { // webpackBootstrap * Retrieve the current custom time. * @return {Date} customTime */ - CustomTime.prototype.getCustomTime = function() { + CustomTime.prototype.getCustomTime = function () { return new Date(this.customTime.valueOf()); }; @@ -19163,7 +19037,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Event} event * @private */ - CustomTime.prototype._onDragStart = function(event) { + CustomTime.prototype._onDragStart = function (event) { this.eventParams.dragging = true; this.eventParams.customTime = this.customTime; @@ -19186,7 +19060,7 @@ return /******/ (function(modules) { // webpackBootstrap this.setCustomTime(time); // fire a timechange event - this.body.emitter.emit('timechange', { + this.body.emitter.emit("timechange", { id: this.options.id, time: new Date(this.customTime.valueOf()) }); @@ -19204,7 +19078,7 @@ return /******/ (function(modules) { // webpackBootstrap if (!this.eventParams.dragging) return; // fire a timechanged event - this.body.emitter.emit('timechanged', { + this.body.emitter.emit("timechanged", { id: this.options.id, time: new Date(this.customTime.valueOf()) }); @@ -19215,32 +19089,34 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = CustomTime; - /***/ }, /* 39 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + // English - exports['en'] = { - current: 'current', - time: 'time' + exports.en = { + current: "current", + time: "time" }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; + exports.en_EN = exports.en; + exports.en_US = exports.en; // Dutch - exports['nl'] = { - custom: 'aangepaste', - time: 'tijd' + exports.nl = { + custom: "aangepaste", + time: "tijd" }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; - + exports.nl_NL = exports.nl; + exports.nl_BE = exports.nl; /***/ }, /* 40 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var Component = __webpack_require__(23); var TimeStep = __webpack_require__(27); @@ -19255,7 +19131,7 @@ return /******/ (function(modules) { // webpackBootstrap * @constructor TimeAxis * @extends Component */ - function TimeAxis (body, options) { + function TimeAxis(body, options) { this.dom = { foreground: null, lines: [], @@ -19277,7 +19153,7 @@ return /******/ (function(modules) { // webpackBootstrap }; this.defaultOptions = { - orientation: 'bottom', // supported: 'top', 'bottom' + orientation: "bottom", // supported: 'top', 'bottom' // TODO: implement timeaxis orientations 'left' and 'right' showMinorLabels: true, showMajorLabels: true, @@ -19304,26 +19180,18 @@ return /******/ (function(modules) { // webpackBootstrap * {boolean} [showMinorLabels] * {boolean} [showMajorLabels] */ - TimeAxis.prototype.setOptions = function(options) { + TimeAxis.prototype.setOptions = function (options) { if (options) { // copy all options that we know - util.selectiveExtend([ - 'orientation', - 'showMinorLabels', - 'showMajorLabels', - 'hiddenDates', - 'format', - 'timeAxis' - ], this.options, options); + util.selectiveExtend(["orientation", "showMinorLabels", "showMajorLabels", "hiddenDates", "format", "timeAxis"], this.options, options); // apply locale to moment.js // TODO: not so nice, this is applied globally to moment.js - if ('locale' in options) { - if (typeof moment.locale === 'function') { + if ("locale" in options) { + if (typeof moment.locale === "function") { // moment.js 2.8.1+ moment.locale(options.locale); - } - else { + } else { moment.lang(options.locale); } } @@ -19333,18 +19201,18 @@ return /******/ (function(modules) { // webpackBootstrap /** * Create the HTML DOM for the TimeAxis */ - TimeAxis.prototype._create = function() { - this.dom.foreground = document.createElement('div'); - this.dom.background = document.createElement('div'); + TimeAxis.prototype._create = function () { + this.dom.foreground = document.createElement("div"); + this.dom.background = document.createElement("div"); - this.dom.foreground.className = 'timeaxis foreground'; - this.dom.background.className = 'timeaxis background'; + this.dom.foreground.className = "timeaxis foreground"; + this.dom.background.className = "timeaxis background"; }; /** * Destroy the TimeAxis */ - TimeAxis.prototype.destroy = function() { + TimeAxis.prototype.destroy = function () { // remove from DOM if (this.dom.foreground.parentNode) { this.dom.foreground.parentNode.removeChild(this.dom.foreground); @@ -19367,8 +19235,8 @@ return /******/ (function(modules) { // webpackBootstrap var background = this.dom.background; // determine the correct parent DOM element (depending on option orientation) - var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom; - var parentChanged = (foreground.parentNode !== parent); + var parent = options.orientation == "top" ? this.body.dom.top : this.body.dom.bottom; + var parentChanged = foreground.parentNode !== parent; // calculate character width and height this._calculateCharSize(); @@ -19384,8 +19252,7 @@ return /******/ (function(modules) { // webpackBootstrap props.height = props.minorLabelHeight + props.majorLabelHeight; props.width = foreground.offsetWidth; - props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - - (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height); + props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - (options.orientation == "top" ? this.body.domProps.bottom.height : this.body.domProps.top.height); props.minorLineWidth = 1; // TODO: really calculate width props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight; props.majorLineWidth = 1; // TODO: really calculate width @@ -19396,22 +19263,20 @@ return /******/ (function(modules) { // webpackBootstrap foreground.parentNode && foreground.parentNode.removeChild(foreground); background.parentNode && background.parentNode.removeChild(background); - foreground.style.height = this.props.height + 'px'; + foreground.style.height = this.props.height + "px"; this._repaintLabels(); // put DOM online again (at the same place) if (foregroundNextSibling) { parent.insertBefore(foreground, foregroundNextSibling); - } - else { - parent.appendChild(foreground) + } else { + parent.appendChild(foreground); } if (backgroundNextSibling) { this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling); - } - else { - this.body.dom.backgroundVertical.appendChild(background) + } else { + this.body.dom.backgroundVertical.appendChild(background); } return this._isResized() || parentChanged; @@ -19425,8 +19290,8 @@ return /******/ (function(modules) { // webpackBootstrap var orientation = this.options.orientation; // calculate range and step (step such that we have space for 7 characters per label) - var start = util.convert(this.body.range.start, 'Number'); - var end = util.convert(this.body.range.end, 'Number'); + var start = util.convert(this.body.range.start, "Number"); + var end = util.convert(this.body.range.end, "Number"); var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf(); var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize); minimumStep -= this.body.util.toTime(0).valueOf(); @@ -19473,7 +19338,7 @@ return /******/ (function(modules) { // webpackBootstrap x = this.body.util.toScreen(cur); width = x - xPrev; if (prevLine) { - prevLine.style.width = width + 'px'; + prevLine.style.width = width + "px"; } if (this.options.showMinorLabels) { @@ -19488,8 +19353,7 @@ return /******/ (function(modules) { // webpackBootstrap this._repaintMajorText(x, step.getLabelMajor(), orientation, className); } prevLine = this._repaintMajorLine(x, orientation, className); - } - else { + } else { prevLine = this._repaintMinorLine(x, orientation, className); } @@ -19532,8 +19396,8 @@ return /******/ (function(modules) { // webpackBootstrap if (!label) { // create new label - var content = document.createTextNode(''); - label = document.createElement('div'); + var content = document.createTextNode(""); + label = document.createElement("div"); label.appendChild(content); this.dom.foreground.appendChild(label); } @@ -19541,9 +19405,9 @@ return /******/ (function(modules) { // webpackBootstrap label.childNodes[0].nodeValue = text; - label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0'; - label.style.left = x + 'px'; - label.className = 'text minor ' + className; + label.style.top = orientation == "top" ? this.props.majorLabelHeight + "px" : "0"; + label.style.left = x + "px"; + label.className = "text minor " + className; //label.title = title; // TODO: this is a heavy operation }; @@ -19562,18 +19426,18 @@ return /******/ (function(modules) { // webpackBootstrap if (!label) { // create label var content = document.createTextNode(text); - label = document.createElement('div'); + label = document.createElement("div"); label.appendChild(content); this.dom.foreground.appendChild(label); } this.dom.majorTexts.push(label); label.childNodes[0].nodeValue = text; - label.className = 'text major ' + className; + label.className = "text major " + className; //label.title = title; // TODO: this is a heavy operation - label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px'); - label.style.left = x + 'px'; + label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; + label.style.left = x + "px"; }; /** @@ -19589,22 +19453,21 @@ return /******/ (function(modules) { // webpackBootstrap var line = this.dom.redundant.lines.shift(); if (!line) { // create vertical line - line = document.createElement('div'); + line = document.createElement("div"); this.dom.background.appendChild(line); } this.dom.lines.push(line); var props = this.props; - if (orientation == 'top') { - line.style.top = props.majorLabelHeight + 'px'; - } - else { - line.style.top = this.body.domProps.top.height + 'px'; + if (orientation == "top") { + line.style.top = props.majorLabelHeight + "px"; + } else { + line.style.top = this.body.domProps.top.height + "px"; } - line.style.height = props.minorLineHeight + 'px'; - line.style.left = (x - props.minorLineWidth / 2) + 'px'; + line.style.height = props.minorLineHeight + "px"; + line.style.left = x - props.minorLineWidth / 2 + "px"; - line.className = 'grid vertical minor ' + className; + line.className = "grid vertical minor " + className; return line; }; @@ -19622,22 +19485,21 @@ return /******/ (function(modules) { // webpackBootstrap var line = this.dom.redundant.lines.shift(); if (!line) { // create vertical line - line = document.createElement('div'); + line = document.createElement("div"); this.dom.background.appendChild(line); } this.dom.lines.push(line); var props = this.props; - if (orientation == 'top') { - line.style.top = '0'; - } - else { - line.style.top = this.body.domProps.top.height + 'px'; + if (orientation == "top") { + line.style.top = "0"; + } else { + line.style.top = this.body.domProps.top.height + "px"; } - line.style.left = (x - props.majorLineWidth / 2) + 'px'; - line.style.height = props.majorLineHeight + 'px'; + line.style.left = x - props.majorLineWidth / 2 + "px"; + line.style.height = props.majorLineHeight + "px"; - line.className = 'grid vertical major ' + className; + line.className = "grid vertical major " + className; return line; }; @@ -19653,11 +19515,11 @@ return /******/ (function(modules) { // webpackBootstrap // determine the char width and height on the minor axis if (!this.dom.measureCharMinor) { - this.dom.measureCharMinor = document.createElement('DIV'); - this.dom.measureCharMinor.className = 'text minor measure'; - this.dom.measureCharMinor.style.position = 'absolute'; + this.dom.measureCharMinor = document.createElement("DIV"); + this.dom.measureCharMinor.className = "text minor measure"; + this.dom.measureCharMinor.style.position = "absolute"; - this.dom.measureCharMinor.appendChild(document.createTextNode('0')); + this.dom.measureCharMinor.appendChild(document.createTextNode("0")); this.dom.foreground.appendChild(this.dom.measureCharMinor); } this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; @@ -19665,11 +19527,11 @@ return /******/ (function(modules) { // webpackBootstrap // determine the char width and height on the major axis if (!this.dom.measureCharMajor) { - this.dom.measureCharMajor = document.createElement('DIV'); - this.dom.measureCharMajor.className = 'text major measure'; - this.dom.measureCharMajor.style.position = 'absolute'; + this.dom.measureCharMajor = document.createElement("DIV"); + this.dom.measureCharMajor.className = "text major measure"; + this.dom.measureCharMajor.style.position = "absolute"; - this.dom.measureCharMajor.appendChild(document.createTextNode('0')); + this.dom.measureCharMajor.appendChild(document.createTextNode("0")); this.dom.foreground.appendChild(this.dom.measureCharMajor); } this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; @@ -19678,11 +19540,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = TimeAxis; - /***/ }, /* 41 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var Component = __webpack_require__(23); var moment = __webpack_require__(2); @@ -19696,7 +19559,7 @@ return /******/ (function(modules) { // webpackBootstrap * @constructor CurrentTime * @extends Component */ - function CurrentTime (body, options) { + function CurrentTime(body, options) { this.body = body; // default options @@ -19704,7 +19567,7 @@ return /******/ (function(modules) { // webpackBootstrap showCurrentTime: true, locales: locales, - locale: 'en' + locale: "en" }; this.options = util.extend({}, this.defaultOptions); this.offset = 0; @@ -19720,12 +19583,12 @@ return /******/ (function(modules) { // webpackBootstrap * Create the HTML DOM for the current time bar * @private */ - CurrentTime.prototype._create = function() { - var bar = document.createElement('div'); - bar.className = 'currenttime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + CurrentTime.prototype._create = function () { + var bar = document.createElement("div"); + bar.className = "currenttime"; + bar.style.position = "absolute"; + bar.style.top = "0px"; + bar.style.height = "100%"; this.bar = bar; }; @@ -19745,10 +19608,10 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} options Available parameters: * {boolean} [showCurrentTime] */ - CurrentTime.prototype.setOptions = function(options) { + CurrentTime.prototype.setOptions = function (options) { if (options) { // copy all options that we know - util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); + util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); } }; @@ -19756,7 +19619,7 @@ return /******/ (function(modules) { // webpackBootstrap * Repaint the component * @return {boolean} Returns true if the component is resized */ - CurrentTime.prototype.redraw = function() { + CurrentTime.prototype.redraw = function () { if (this.options.showCurrentTime) { var parent = this.body.dom.backgroundVertical; if (this.bar.parentNode != parent) { @@ -19773,13 +19636,12 @@ return /******/ (function(modules) { // webpackBootstrap var x = this.body.util.toScreen(now); var locale = this.options.locales[this.options.locale]; - var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); + var title = locale.current + " " + locale.time + ": " + moment(now).format("dddd, MMMM Do YYYY, H:mm:ss"); title = title.charAt(0).toUpperCase() + title.substring(1); - this.bar.style.left = x + 'px'; + this.bar.style.left = x + "px"; this.bar.title = title; - } - else { + } else { // remove the line from the DOM if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); @@ -19793,21 +19655,21 @@ return /******/ (function(modules) { // webpackBootstrap /** * Start auto refreshing the current time bar */ - CurrentTime.prototype.start = function() { + CurrentTime.prototype.start = function () { var me = this; - function update () { + function update() { me.stop(); // determine interval to refresh var scale = me.body.range.conversion(me.body.domProps.center.width).scale; var interval = 1 / scale / 10; - if (interval < 30) interval = 30; + if (interval < 30) interval = 30; if (interval > 1000) interval = 1000; me.redraw(); - // start a timer to adjust for the new time + // start a renderTimer to adjust for the new time me.currentTimeTimer = setTimeout(update, interval); } @@ -19817,7 +19679,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Stop auto refreshing the current time bar */ - CurrentTime.prototype.stop = function() { + CurrentTime.prototype.stop = function () { if (this.currentTimeTimer !== undefined) { clearTimeout(this.currentTimeTimer); delete this.currentTimeTimer; @@ -19830,8 +19692,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Date | String | Number} time A Date, unix timestamp, or * ISO date string. */ - CurrentTime.prototype.setCurrentTime = function(time) { - var t = util.convert(time, 'Date').valueOf(); + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, "Date").valueOf(); var now = new Date().valueOf(); this.offset = t - now; this.redraw(); @@ -19841,17 +19703,18 @@ return /******/ (function(modules) { // webpackBootstrap * Get the current time. * @return {Date} Returns the current time. */ - CurrentTime.prototype.getCurrentTime = function() { + CurrentTime.prototype.getCurrentTime = function () { return new Date(new Date().valueOf() + this.offset); }; module.exports = CurrentTime; - /***/ }, /* 42 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Emitter = __webpack_require__(11); var Hammer = __webpack_require__(19); var util = __webpack_require__(1); @@ -19872,7 +19735,7 @@ return /******/ (function(modules) { // webpackBootstrap * @constructor * @extends Core */ - function Graph2d (container, items, groups, options) { + function Graph2d(container, items, groups, options) { // if the third element is options, the forth is groups (optionally); if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) { var forthArgument = options; @@ -19883,11 +19746,11 @@ return /******/ (function(modules) { // webpackBootstrap var me = this; this.defaultOptions = { start: null, - end: null, + end: null, autoResize: true, - orientation: 'bottom', + orientation: "bottom", width: null, height: null, maxHeight: null, @@ -19914,7 +19777,7 @@ return /******/ (function(modules) { // webpackBootstrap toScreen: me._toScreen.bind(me), toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toTime: me._toTime.bind(me), - toGlobalTime : me._toGlobalTime.bind(me) + toGlobalTime: me._toGlobalTime.bind(me) } }; @@ -19941,8 +19804,8 @@ return /******/ (function(modules) { // webpackBootstrap this.linegraph = new LineGraph(this.body); this.components.push(this.linegraph); - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet // apply options if (options) { @@ -19957,8 +19820,7 @@ return /******/ (function(modules) { // webpackBootstrap // create itemset if (items) { this.setItems(items); - } - else { + } else { this._redraw(); } } @@ -19970,23 +19832,21 @@ return /******/ (function(modules) { // webpackBootstrap * Set items * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - Graph2d.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); + Graph2d.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; // convert to type DataSet when needed var newDataSet; if (!items) { newDataSet = null; - } - else if (items instanceof DataSet || items instanceof DataView) { + } else if (items instanceof DataSet || items instanceof DataView) { newDataSet = items; - } - else { + } else { // turn an array into a dataset newDataSet = new DataSet(items, { type: { - start: 'Date', - end: 'Date' + start: "Date", + end: "Date" } }); } @@ -19998,12 +19858,11 @@ return /******/ (function(modules) { // webpackBootstrap if (initialLoad) { if (this.options.start != undefined || this.options.end != undefined) { var start = this.options.start != undefined ? this.options.start : null; - var end = this.options.end != undefined ? this.options.end : null; + var end = this.options.end != undefined ? this.options.end : null; - this.setWindow(start, end, {animate: false}); - } - else { - this.fit({animate: false}); + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); } } }; @@ -20012,16 +19871,14 @@ return /******/ (function(modules) { // webpackBootstrap * Set groups * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - Graph2d.prototype.setGroups = function(groups) { + Graph2d.prototype.setGroups = function (groups) { // convert to type DataSet when needed var newDataSet; if (!groups) { newDataSet = null; - } - else if (groups instanceof DataSet || groups instanceof DataView) { + } else if (groups instanceof DataSet || groups instanceof DataView) { newDataSet = groups; - } - else { + } else { // turn an array into a dataset newDataSet = new DataSet(groups); } @@ -20036,30 +19893,32 @@ return /******/ (function(modules) { // webpackBootstrap * @param width * @param height */ - Graph2d.prototype.getLegend = function(groupId, width, height) { - if (width === undefined) {width = 15;} - if (height === undefined) {height = 15;} - if (this.linegraph.groups[groupId] !== undefined) { - return this.linegraph.groups[groupId].getLegend(width,height); + Graph2d.prototype.getLegend = function (groupId, width, height) { + if (width === undefined) { + width = 15; } - else { - return "cannot find group:" + groupId; + if (height === undefined) { + height = 15; } - } + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].getLegend(width, height); + } else { + return "cannot find group:" + groupId; + } + }; /** * This checks if the visible option of the supplied group (by ID) is true or false. * @param groupId * @returns {*} */ - Graph2d.prototype.isGroupVisible = function(groupId) { + Graph2d.prototype.isGroupVisible = function (groupId) { if (this.linegraph.groups[groupId] !== undefined) { - return (this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true)); - } - else { + return this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true); + } else { return false; } - } + }; /** @@ -20068,7 +19927,7 @@ return /******/ (function(modules) { // webpackBootstrap * When no minimum is found, min==null * When no maximum is found, max==null */ - Graph2d.prototype.getItemRange = function() { + Graph2d.prototype.getItemRange = function () { var min = null; var max = null; @@ -20078,7 +19937,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.linegraph.groups[groupId].visible == true) { for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { var item = this.linegraph.groups[groupId].itemsData[i]; - var value = util.convert(item.x, 'Date').valueOf(); + var value = util.convert(item.x, "Date").valueOf(); min = min == null ? value : min > value ? value : min; max = max == null ? value : max < value ? value : max; } @@ -20087,8 +19946,8 @@ return /******/ (function(modules) { // webpackBootstrap } return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null }; }; @@ -20096,11 +19955,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Graph2d; - /***/ }, /* 43 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); var DataSet = __webpack_require__(7); @@ -20111,7 +19971,7 @@ return /******/ (function(modules) { // webpackBootstrap var Legend = __webpack_require__(50); var BarGraphFunctions = __webpack_require__(49); - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items /** * This is the constructor of the LineGraph. It requires a Timeline body and options. @@ -20125,41 +19985,41 @@ return /******/ (function(modules) { // webpackBootstrap this.body = body; this.defaultOptions = { - yAxisOrientation: 'left', - defaultGroup: 'default', + yAxisOrientation: "left", + defaultGroup: "default", sort: true, sampling: true, - graphHeight: '400px', + graphHeight: "400px", shaded: { enabled: false, - orientation: 'bottom' // top, bottom + orientation: "bottom" // top, bottom }, - style: 'line', // line, bar + style: "line", // line, bar barChart: { width: 50, - handleOverlap: 'overlap', - align: 'center' // left, center, right + handleOverlap: "overlap", + align: "center" // left, center, right }, catmullRom: { enabled: true, - parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) + parametrization: "centripetal", // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) alpha: 0.5 }, drawPoints: { enabled: true, size: 6, - style: 'square' // square, circle + style: "square" // square, circle }, dataAxis: { showMinorLabels: true, showMajorLabels: true, icons: false, - width: '40px', + width: "40px", visible: true, alignZeros: true, customRange: { - left: {min:undefined, max:undefined}, - right: {min:undefined, max:undefined} + left: { min: undefined, max: undefined }, + right: { min: undefined, max: undefined } } //, these options are not set by default, but this shows the format they will be in //format: { @@ -20182,11 +20042,11 @@ return /******/ (function(modules) { // webpackBootstrap icons: true, left: { visible: true, - position: 'top-left' // top/bottom - left,right + position: "top-left" // top/bottom - left,right }, right: { visible: true, - position: 'top-right' // top/bottom - left,right + position: "top-right" // top/bottom - left,right } }, groups: { @@ -20205,37 +20065,37 @@ return /******/ (function(modules) { // webpackBootstrap this.updateSVGheightOnResize = false; var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet // listeners for the DataSet of the items this.itemListeners = { - 'add': function (event, params, senderId) { + add: function (event, params, senderId) { me._onAdd(params.items); }, - 'update': function (event, params, senderId) { + update: function (event, params, senderId) { me._onUpdate(params.items); }, - 'remove': function (event, params, senderId) { + remove: function (event, params, senderId) { me._onRemove(params.items); } }; // listeners for the DataSet of the groups this.groupListeners = { - 'add': function (event, params, senderId) { + add: function (event, params, senderId) { me._onAddGroups(params.items); }, - 'update': function (event, params, senderId) { + update: function (event, params, senderId) { me._onUpdateGroups(params.items); }, - 'remove': function (event, params, senderId) { + remove: function (event, params, senderId) { me._onRemoveGroups(params.items); } }; - this.items = {}; // object with an Item for every data item - this.selection = []; // list with the ids of all selected nodes + this.items = {}; // object with an Item for every data item + this.selection = []; // list with the ids of all selected nodes this.lastStart = this.body.range.start; this.touchParams = {}; // stores properties while dragging @@ -20243,17 +20103,16 @@ return /******/ (function(modules) { // webpackBootstrap this.setOptions(options); this.groupsUsingDefaultStyles = [0]; this.COUNTER = 0; - this.body.emitter.on('rangechanged', function() { + this.body.emitter.on("rangechanged", function () { me.lastStart = me.body.range.start; me.svg.style.left = util.option.asSize(-me.props.width); - me.redraw.call(me,true); + me.redraw.call(me, true); }); // create the HTML DOM this._create(); - this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups}; - this.body.emitter.emit('change'); - + this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; + this.body.emitter.emit("change"); } LineGraph.prototype = new Component(); @@ -20261,29 +20120,29 @@ return /******/ (function(modules) { // webpackBootstrap /** * Create the HTML DOM for the ItemSet */ - LineGraph.prototype._create = function(){ - var frame = document.createElement('div'); - frame.className = 'LineGraph'; + LineGraph.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "LineGraph"; this.dom.frame = frame; // create svg element for graph drawing. - this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); - this.svg.style.position = 'relative'; - this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px'; - this.svg.style.display = 'block'; + this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.svg.style.position = "relative"; + this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; + this.svg.style.display = "block"; frame.appendChild(this.svg); // data axis - this.options.dataAxis.orientation = 'left'; + this.options.dataAxis.orientation = "left"; this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - this.options.dataAxis.orientation = 'right'; + this.options.dataAxis.orientation = "right"; this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); delete this.options.dataAxis.orientation; // legends - this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups); - this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups); + this.legendLeft = new Legend(this.body, this.options.legend, "left", this.options.groups); + this.legendRight = new Legend(this.body, this.options.legend, "right", this.options.groups); this.show(); }; @@ -20292,35 +20151,32 @@ return /******/ (function(modules) { // webpackBootstrap * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. * @param {object} options */ - LineGraph.prototype.setOptions = function(options) { + LineGraph.prototype.setOptions = function (options) { if (options) { - var fields = ['sampling','defaultGroup','height','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups']; + var fields = ["sampling", "defaultGroup", "height", "graphHeight", "yAxisOrientation", "style", "barChart", "dataAxis", "sort", "groups"]; if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { this.updateSVGheight = true; this.updateSVGheightOnResize = true; - } - else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { - if (parseInt((options.graphHeight + '').replace("px",'')) < this.body.domProps.centerContainer.height) { + } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { + if (parseInt((options.graphHeight + "").replace("px", "")) < this.body.domProps.centerContainer.height) { this.updateSVGheight = true; } } util.selectiveDeepExtend(fields, this.options, options); - util.mergeOptions(this.options, options,'catmullRom'); - util.mergeOptions(this.options, options,'drawPoints'); - util.mergeOptions(this.options, options,'shaded'); - util.mergeOptions(this.options, options,'legend'); + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); + util.mergeOptions(this.options, options, "legend"); if (options.catmullRom) { - if (typeof options.catmullRom == 'object') { + if (typeof options.catmullRom == "object") { if (options.catmullRom.parametrization) { - if (options.catmullRom.parametrization == 'uniform') { + if (options.catmullRom.parametrization == "uniform") { this.options.catmullRom.alpha = 0; - } - else if (options.catmullRom.parametrization == 'chordal') { - this.options.catmullRom.alpha = 1.0; - } - else { - this.options.catmullRom.parametrization = 'centripetal'; + } else if (options.catmullRom.parametrization == "chordal") { + this.options.catmullRom.alpha = 1; + } else { + this.options.catmullRom.parametrization = "centripetal"; this.options.catmullRom.alpha = 0.5; } } @@ -20355,7 +20211,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the component from the DOM */ - LineGraph.prototype.hide = function() { + LineGraph.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); @@ -20367,7 +20223,7 @@ return /******/ (function(modules) { // webpackBootstrap * Show the component in the DOM (when not already visible). * @return {Boolean} changed */ - LineGraph.prototype.show = function() { + LineGraph.prototype.show = function () { // show frame containing the items if (!this.dom.frame.parentNode) { this.body.dom.center.appendChild(this.dom.frame); @@ -20379,20 +20235,18 @@ return /******/ (function(modules) { // webpackBootstrap * Set items * @param {vis.DataSet | null} items */ - LineGraph.prototype.setItems = function(items) { + LineGraph.prototype.setItems = function (items) { var me = this, - ids, - oldItemsData = this.itemsData; + ids, + oldItemsData = this.itemsData; // replace the dataset if (!items) { this.itemsData = null; - } - else if (items instanceof DataSet || items instanceof DataView) { + } else if (items instanceof DataSet || items instanceof DataView) { this.itemsData = items; - } - else { - throw new TypeError('Data must be an instance of DataSet or DataView'); + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); } if (oldItemsData) { @@ -20427,7 +20281,7 @@ return /******/ (function(modules) { // webpackBootstrap * Set groups * @param {vis.DataSet} groups */ - LineGraph.prototype.setGroups = function(groups) { + LineGraph.prototype.setGroups = function (groups) { var me = this; var ids; @@ -20446,12 +20300,10 @@ return /******/ (function(modules) { // webpackBootstrap // replace the dataset if (!groups) { this.groupsData = null; - } - else if (groups instanceof DataSet || groups instanceof DataView) { + } else if (groups instanceof DataSet || groups instanceof DataView) { this.groupsData = groups; - } - else { - throw new TypeError('Data must be an instance of DataSet or DataView'); + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); } if (this.groupsData) { @@ -20474,15 +20326,19 @@ return /******/ (function(modules) { // webpackBootstrap * @param [ids] * @private */ - LineGraph.prototype._onUpdate = function(ids) { + LineGraph.prototype._onUpdate = function (ids) { this._updateUngrouped(); this._updateAllGroupData(); //this._updateGraph(); this.redraw(true); }; - LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);}; - LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);}; - LineGraph.prototype._onUpdateGroups = function (groupIds) { + LineGraph.prototype._onAdd = function (ids) { + this._onUpdate(ids); + }; + LineGraph.prototype._onRemove = function (ids) { + this._onUpdate(ids); + }; + LineGraph.prototype._onUpdateGroups = function (groupIds) { for (var i = 0; i < groupIds.length; i++) { var group = this.groupsData.get(groupIds[i]); this._updateGroup(group, groupIds[i]); @@ -20491,7 +20347,9 @@ return /******/ (function(modules) { // webpackBootstrap //this._updateGraph(); this.redraw(true); }; - LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);}; + LineGraph.prototype._onAddGroups = function (groupIds) { + this._onUpdateGroups(groupIds); + }; /** @@ -20502,12 +20360,11 @@ return /******/ (function(modules) { // webpackBootstrap LineGraph.prototype._onRemoveGroups = function (groupIds) { for (var i = 0; i < groupIds.length; i++) { if (this.groups.hasOwnProperty(groupIds[i])) { - if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') { + if (this.groups[groupIds[i]].options.yAxisOrientation == "right") { this.yAxisRight.removeGroup(groupIds[i]); this.legendRight.removeGroup(groupIds[i]); this.legendRight.redraw(); - } - else { + } else { this.yAxisLeft.removeGroup(groupIds[i]); this.legendLeft.removeGroup(groupIds[i]); this.legendLeft.redraw(); @@ -20531,22 +20388,19 @@ return /******/ (function(modules) { // webpackBootstrap LineGraph.prototype._updateGroup = function (group, groupId) { if (!this.groups.hasOwnProperty(groupId)) { this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); - if (this.groups[groupId].options.yAxisOrientation == 'right') { + if (this.groups[groupId].options.yAxisOrientation == "right") { this.yAxisRight.addGroup(groupId, this.groups[groupId]); this.legendRight.addGroup(groupId, this.groups[groupId]); - } - else { + } else { this.yAxisLeft.addGroup(groupId, this.groups[groupId]); this.legendLeft.addGroup(groupId, this.groups[groupId]); } - } - else { + } else { this.groups[groupId].update(group); - if (this.groups[groupId].options.yAxisOrientation == 'right') { + if (this.groups[groupId].options.yAxisOrientation == "right") { this.yAxisRight.updateGroup(groupId, this.groups[groupId]); this.legendRight.updateGroup(groupId, this.groups[groupId]); - } - else { + } else { this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); this.legendLeft.updateGroup(groupId, this.groups[groupId]); } @@ -20574,9 +20428,9 @@ return /******/ (function(modules) { // webpackBootstrap if (this.itemsData._data.hasOwnProperty(itemId)) { var item = this.itemsData._data[itemId]; if (groupsContent[item.group] === undefined) { - throw new Error('Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups.') + throw new Error("Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups."); } - item.x = util.convert(item.x,'Date'); + item.x = util.convert(item.x, "Date"); groupsContent[item.group].push(item); } } @@ -20594,19 +20448,18 @@ return /******/ (function(modules) { // webpackBootstrap * there are no groups specified. This anonymous group is called 'graph'. * @protected */ - LineGraph.prototype._updateUngrouped = function() { + LineGraph.prototype._updateUngrouped = function () { if (this.itemsData && this.itemsData != null) { var ungroupedCounter = 0; for (var itemId in this.itemsData._data) { if (this.itemsData._data.hasOwnProperty(itemId)) { var item = this.itemsData._data[itemId]; if (item != undefined) { - if (item.hasOwnProperty('group')) { + if (item.hasOwnProperty("group")) { if (item.group === undefined) { item.group = UNGROUPED; } - } - else { + } else { item.group = UNGROUPED; } ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter; @@ -20620,13 +20473,11 @@ return /******/ (function(modules) { // webpackBootstrap this.legendRight.removeGroup(UNGROUPED); this.yAxisLeft.removeGroup(UNGROUPED); this.yAxisRight.removeGroup(UNGROUPED); - } - else { - var group = {id: UNGROUPED, content: this.options.defaultGroup}; + } else { + var group = { id: UNGROUPED, content: this.options.defaultGroup }; this._updateGroup(group, UNGROUPED); } - } - else { + } else { delete this.groups[UNGROUPED]; this.legendLeft.removeGroup(UNGROUPED); this.legendRight.removeGroup(UNGROUPED); @@ -20643,7 +20494,7 @@ return /******/ (function(modules) { // webpackBootstrap * Redraw the component, mandatory function * @return {boolean} Returns true if the component is resized */ - LineGraph.prototype.redraw = function(forceGraphUpdate) { + LineGraph.prototype.redraw = function (forceGraphUpdate) { var resized = false; // calculate actual size and position @@ -20660,47 +20511,45 @@ return /******/ (function(modules) { // webpackBootstrap // check whether zoomed (in that case we need to re-stack everything) var visibleInterval = this.body.range.end - this.body.range.start; - var zoomed = (visibleInterval != this.lastVisibleInterval); + var zoomed = visibleInterval != this.lastVisibleInterval; this.lastVisibleInterval = visibleInterval; // the svg element is three times as big as the width, this allows for fully dragging left and right // without reloading the graph. the controls for this are bound to events in the constructor if (resized == true) { - this.svg.style.width = util.option.asSize(3*this.props.width); + this.svg.style.width = util.option.asSize(3 * this.props.width); this.svg.style.left = util.option.asSize(-this.props.width); // if the height of the graph is set as proportional, change the height of the svg - if ((this.options.height + '').indexOf("%") != -1 || this.updateSVGheightOnResize == true) { + if ((this.options.height + "").indexOf("%") != -1 || this.updateSVGheightOnResize == true) { this.updateSVGheight = true; } } // update the height of the graph on each redraw of the graph. if (this.updateSVGheight == true) { - if (this.options.graphHeight != this.body.domProps.centerContainer.height + 'px') { - this.options.graphHeight = this.body.domProps.centerContainer.height + 'px'; - this.svg.style.height = this.body.domProps.centerContainer.height + 'px'; + if (this.options.graphHeight != this.body.domProps.centerContainer.height + "px") { + this.options.graphHeight = this.body.domProps.centerContainer.height + "px"; + this.svg.style.height = this.body.domProps.centerContainer.height + "px"; } this.updateSVGheight = false; - } - else { - this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px'; + } else { + this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; } // zoomed is here to ensure that animations are shown correctly. if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { resized = this._updateGraph() || resized; - } - else { + } else { // move the whole svg while dragging if (this.lastStart != 0) { var offset = this.body.range.start - this.lastStart; var range = this.body.range.end - this.body.range.start; if (this.props.width != 0) { - var rangePerPixelInv = this.props.width/range; + var rangePerPixelInv = this.props.width / range; var xOffset = offset * rangePerPixelInv; - this.svg.style.left = (-this.props.width - xOffset) + 'px'; + this.svg.style.left = -this.props.width - xOffset + "px"; } } } @@ -20762,12 +20611,11 @@ return /******/ (function(modules) { // webpackBootstrap DOMutil.cleanupElements(this.svgElements); this.abortedGraphUpdate = true; this.COUNTER++; - this.body.emitter.emit('change'); + this.body.emitter.emit("change"); return true; - } - else { + } else { if (this.COUNTER > MAX_CYCLES) { - console.log("WARNING: there may be an infinite loop in the _updateGraph emitter cycle.") + console.log("WARNING: there may be an infinite loop in the _updateGraph emitter cycle."); } this.COUNTER = 0; this.abortedGraphUpdate = false; @@ -20781,7 +20629,8 @@ return /******/ (function(modules) { // webpackBootstrap // draw the groups for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; - if (group.options.style != 'bar') { // bar needs to be drawn enmasse + if (group.options.style != "bar") { + // bar needs to be drawn enmasse group.draw(processedGroupData[groupIds[i]], group, this.framework); } } @@ -20818,21 +20667,19 @@ return /******/ (function(modules) { // webpackBootstrap var dataContainer = groupsData[groupIds[i]]; // optimization for sorted data if (group.options.sort == true) { - var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, 'x', 'before')); + var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, "x", "before")); for (j = guess; j < group.itemsData.length; j++) { item = group.itemsData[j]; if (item !== undefined) { if (item.x > maxDate) { dataContainer.push(item); break; - } - else { + } else { dataContainer.push(item); } } } - } - else { + } else { for (j = 0; j < group.itemsData.length; j++) { item = group.itemsData[j]; if (item !== undefined) { @@ -20873,7 +20720,6 @@ return /******/ (function(modules) { // webpackBootstrap var sampledData = []; for (var j = 0; j < amountOfPoints; j += increment) { sampledData.push(dataContainer[j]); - } groupsData[groupIds[i]] = sampledData; } @@ -20903,19 +20749,21 @@ return /******/ (function(modules) { // webpackBootstrap if (groupData.length > 0) { group = this.groups[groupIds[i]]; // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - if (options.barChart.handleOverlap == 'stack' && options.style == 'bar') { - if (options.yAxisOrientation == 'left') {barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)) ;} - else {barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData));} - } - else { - groupRanges[groupIds[i]] = group.getYRange(groupData,groupIds[i]); + if (options.barChart.handleOverlap == "stack" && options.style == "bar") { + if (options.yAxisOrientation == "left") { + barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)); + } else { + barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData)); + } + } else { + groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); } } } // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft , groupRanges, groupIds, '__barchartLeft' , 'left' ); - BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, '__barchartRight', 'right'); + BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft, groupRanges, groupIds, "__barchartLeft", "left"); + BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, "__barchartRight", "right"); } }; @@ -20930,18 +20778,22 @@ return /******/ (function(modules) { // webpackBootstrap var resized = false; var yAxisLeftUsed = false; var yAxisRightUsed = false; - var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal; + var minLeft = 1000000000, + minRight = 1000000000, + maxLeft = -1000000000, + maxRight = -1000000000, + minVal, + maxVal; // if groups are present if (groupIds.length > 0) { // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. for (var i = 0; i < groupIds.length; i++) { var group = this.groups[groupIds[i]]; - if (group && group.options.yAxisOrientation != 'right') { + if (group && group.options.yAxisOrientation != "right") { yAxisLeftUsed = true; minLeft = 0; maxLeft = 0; - } - else if (group && group.options.yAxisOrientation) { + } else if (group && group.options.yAxisOrientation) { yAxisRightUsed = true; minRight = 0; maxRight = 0; @@ -20955,12 +20807,11 @@ return /******/ (function(modules) { // webpackBootstrap minVal = groupRanges[groupIds[i]].min; maxVal = groupRanges[groupIds[i]].max; - if (groupRanges[groupIds[i]].yAxisOrientation != 'right') { + if (groupRanges[groupIds[i]].yAxisOrientation != "right") { yAxisLeftUsed = true; minLeft = minLeft > minVal ? minVal : minLeft; maxLeft = maxLeft < maxVal ? maxVal : maxLeft; - } - else { + } else { yAxisRightUsed = true; minRight = minRight > minVal ? minVal : minRight; maxRight = maxRight < maxVal ? maxVal : maxRight; @@ -20976,37 +20827,38 @@ return /******/ (function(modules) { // webpackBootstrap this.yAxisRight.setRange(minRight, maxRight); } } - resized = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || resized; + resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; if (yAxisRightUsed == true && yAxisLeftUsed == true) { this.yAxisLeft.drawIcons = true; this.yAxisRight.drawIcons = true; - } - else { + } else { this.yAxisLeft.drawIcons = false; this.yAxisRight.drawIcons = false; } this.yAxisRight.master = !yAxisLeftUsed; if (this.yAxisRight.master == false) { - if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;} - else {this.yAxisLeft.lineOffset = 0;} + if (yAxisRightUsed == true) { + this.yAxisLeft.lineOffset = this.yAxisRight.width; + } else { + this.yAxisLeft.lineOffset = 0; + } resized = this.yAxisLeft.redraw() || resized; this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels; this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; resized = this.yAxisRight.redraw() || resized; - } - else { + } else { resized = this.yAxisRight.redraw() || resized; } // clean the accumulated lists - if (groupIds.indexOf('__barchartLeft') != -1) { - groupIds.splice(groupIds.indexOf('__barchartLeft'),1); + if (groupIds.indexOf("__barchartLeft") != -1) { + groupIds.splice(groupIds.indexOf("__barchartLeft"), 1); } - if (groupIds.indexOf('__barchartRight') != -1) { - groupIds.splice(groupIds.indexOf('__barchartRight'),1); + if (groupIds.indexOf("__barchartRight") != -1) { + groupIds.splice(groupIds.indexOf("__barchartRight"), 1); } return resized; @@ -21025,11 +20877,10 @@ return /******/ (function(modules) { // webpackBootstrap var changed = false; if (axisUsed == false) { if (axis.dom.frame.parentNode && axis.hidden == false) { - axis.hide() + axis.hide(); changed = true; } - } - else { + } else { if (!axis.dom.frame.parentNode && axis.hidden == true) { axis.show(); changed = true; @@ -21056,7 +20907,7 @@ return /******/ (function(modules) { // webpackBootstrap for (var i = 0; i < datapoints.length; i++) { xValue = toScreen(datapoints[i].x) + this.props.width; yValue = datapoints[i].y; - extractedData.push({x: xValue, y: yValue}); + extractedData.push({ x: xValue, y: yValue }); } return extractedData; @@ -21078,8 +20929,8 @@ return /******/ (function(modules) { // webpackBootstrap var xValue, yValue; var toScreen = this.body.util.toScreen; var axis = this.yAxisLeft; - var svgHeight = Number(this.svg.style.height.replace('px','')); - if (group.options.yAxisOrientation == 'right') { + var svgHeight = Number(this.svg.style.height.replace("px", "")); + if (group.options.yAxisOrientation == "right") { axis = this.yAxisRight; } @@ -21091,10 +20942,10 @@ return /******/ (function(modules) { // webpackBootstrap //else { // labelValue = null; //} - labelValue = datapoints[i].label ? datapoints[i].label : null; + labelValue = datapoints[i].label ? datapoints[i].label : null; xValue = toScreen(datapoints[i].x) + this.props.width; yValue = Math.round(axis.convertValue(datapoints[i].y)); - extractedData.push({x: xValue, y: yValue, label:labelValue}); + extractedData.push({ x: xValue, y: yValue, label: labelValue }); } group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); @@ -21105,11 +20956,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = LineGraph; - /***/ }, /* 44 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); var Component = __webpack_require__(23); @@ -21123,12 +20975,12 @@ return /******/ (function(modules) { // webpackBootstrap * @extends Component * @param body */ - function DataAxis (body, options, svg, linegraphOptions) { + function DataAxis(body, options, svg, linegraphOptions) { this.id = util.randomUUID(); this.body = body; this.defaultOptions = { - orientation: 'left', // supported: 'left', 'right' + orientation: "left", // supported: 'left', 'right' showMinorLabels: true, showMajorLabels: true, icons: true, @@ -21137,20 +20989,20 @@ return /******/ (function(modules) { // webpackBootstrap labelOffsetX: 10, labelOffsetY: 2, iconWidth: 20, - width: '40px', + width: "40px", visible: true, alignZeros: true, customRange: { - left: {min:undefined, max:undefined}, - right: {min:undefined, max:undefined} + left: { min: undefined, max: undefined }, + right: { min: undefined, max: undefined } }, title: { - left: {text:undefined}, - right: {text:undefined} + left: { text: undefined }, + right: { text: undefined } }, format: { - left: {decimals: undefined}, - right: {decimals: undefined} + left: { decimals: undefined }, + right: { decimals: undefined } } }; @@ -21165,13 +21017,13 @@ return /******/ (function(modules) { // webpackBootstrap this.dom = {}; - this.range = {start:0, end:0}; + this.range = { start: 0, end: 0 }; this.options = util.extend({}, this.defaultOptions); this.conversionFactor = 1; this.setOptions(options); - this.width = Number(('' + this.options.width).replace("px","")); + this.width = Number(("" + this.options.width).replace("px", "")); this.minWidth = this.width; this.height = this.linegraphSVG.offsetHeight; this.hidden = false; @@ -21193,26 +21045,26 @@ return /******/ (function(modules) { // webpackBootstrap this._create(); var me = this; - this.body.emitter.on("verticalDrag", function() { - me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px'; + this.body.emitter.on("verticalDrag", function () { + me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; }); } DataAxis.prototype = new Component(); - DataAxis.prototype.addGroup = function(label, graphOptions) { + DataAxis.prototype.addGroup = function (label, graphOptions) { if (!this.groups.hasOwnProperty(label)) { this.groups[label] = graphOptions; } this.amountOfGroups += 1; }; - DataAxis.prototype.updateGroup = function(label, graphOptions) { + DataAxis.prototype.updateGroup = function (label, graphOptions) { this.groups[label] = graphOptions; }; - DataAxis.prototype.removeGroup = function(label) { + DataAxis.prototype.removeGroup = function (label) { if (this.groups.hasOwnProperty(label)) { delete this.groups[label]; this.amountOfGroups -= 1; @@ -21226,26 +21078,10 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.orientation != options.orientation && options.orientation !== undefined) { redraw = true; } - var fields = [ - 'orientation', - 'showMinorLabels', - 'showMajorLabels', - 'icons', - 'majorLinesOffset', - 'minorLinesOffset', - 'labelOffsetX', - 'labelOffsetY', - 'iconWidth', - 'width', - 'visible', - 'customRange', - 'title', - 'format', - 'alignZeros' - ]; + var fields = ["orientation", "showMinorLabels", "showMajorLabels", "icons", "majorLinesOffset", "minorLinesOffset", "labelOffsetX", "labelOffsetY", "iconWidth", "width", "visible", "customRange", "title", "format", "alignZeros"]; util.selectiveExtend(fields, this.options, options); - this.minWidth = Number(('' + this.options.width).replace("px","")); + this.minWidth = Number(("" + this.options.width).replace("px", "")); if (redraw == true && this.dom.frame) { this.hide(); @@ -21258,22 +21094,22 @@ return /******/ (function(modules) { // webpackBootstrap /** * Create the HTML DOM for the DataAxis */ - DataAxis.prototype._create = function() { - this.dom.frame = document.createElement('div'); + DataAxis.prototype._create = function () { + this.dom.frame = document.createElement("div"); this.dom.frame.style.width = this.options.width; this.dom.frame.style.height = this.height; - this.dom.lineContainer = document.createElement('div'); - this.dom.lineContainer.style.width = '100%'; + this.dom.lineContainer = document.createElement("div"); + this.dom.lineContainer.style.width = "100%"; this.dom.lineContainer.style.height = this.height; - this.dom.lineContainer.style.position = 'relative'; + this.dom.lineContainer.style.position = "relative"; // create svg element for graph drawing. - this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg"); + this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.svg.style.position = "absolute"; - this.svg.style.top = '0px'; - this.svg.style.height = '100%'; - this.svg.style.width = '100%'; + this.svg.style.top = "0px"; + this.svg.style.height = "100%"; + this.svg.style.width = "100%"; this.svg.style.display = "block"; this.dom.frame.appendChild(this.svg); }; @@ -21287,10 +21123,9 @@ return /******/ (function(modules) { // webpackBootstrap var iconOffset = 4; var y = iconOffset + 0.5 * iconHeight; - if (this.options.orientation == 'left') { + if (this.options.orientation == "left") { x = iconOffset; - } - else { + } else { x = this.width - iconWidth - iconOffset; } @@ -21307,24 +21142,23 @@ return /******/ (function(modules) { // webpackBootstrap this.iconsRemoved = false; }; - DataAxis.prototype._cleanupIcons = function() { + DataAxis.prototype._cleanupIcons = function () { if (this.iconsRemoved == false) { DOMutil.prepareElements(this.svgElements); DOMutil.cleanupElements(this.svgElements); this.iconsRemoved = true; } - } + }; /** * Create the HTML DOM for the DataAxis */ - DataAxis.prototype.show = function() { + DataAxis.prototype.show = function () { this.hidden = false; if (!this.dom.frame.parentNode) { - if (this.options.orientation == 'left') { + if (this.options.orientation == "left") { this.body.dom.left.appendChild(this.dom.frame); - } - else { + } else { this.body.dom.right.appendChild(this.dom.frame); } } @@ -21337,7 +21171,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Create the HTML DOM for the DataAxis */ - DataAxis.prototype.hide = function() { + DataAxis.prototype.hide = function () { this.hidden = true; if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); @@ -21371,9 +21205,9 @@ return /******/ (function(modules) { // webpackBootstrap DataAxis.prototype.redraw = function () { var resized = false; var activeGroups = 0; - + // Make sure the line container adheres to the vertical scrolling. - this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px'; + this.dom.lineContainer.style.top = this.body.domProps.scrollTop + "px"; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { @@ -21384,20 +21218,19 @@ return /******/ (function(modules) { // webpackBootstrap } if (this.amountOfGroups == 0 || activeGroups == 0) { this.hide(); - } - else { + } else { this.show(); - this.height = Number(this.linegraphSVG.style.height.replace("px","")); + this.height = Number(this.linegraphSVG.style.height.replace("px", "")); // svg offsetheight did not work in firefox and explorer... - this.dom.lineContainer.style.height = this.height + 'px'; - this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0; + this.dom.lineContainer.style.height = this.height + "px"; + this.width = this.options.visible == true ? Number(("" + this.options.width).replace("px", "")) : 0; var props = this.props; var frame = this.dom.frame; // update classname - frame.className = 'dataaxis'; + frame.className = "dataaxis"; // calculate character width and height this._calculateCharSize(); @@ -21416,20 +21249,20 @@ return /******/ (function(modules) { // webpackBootstrap props.majorLineHeight = 1; // take frame offline while updating (is almost twice as fast) - if (orientation == 'left') { - frame.style.top = '0'; - frame.style.left = '0'; - frame.style.bottom = ''; - frame.style.width = this.width + 'px'; + if (orientation == "left") { + frame.style.top = "0"; + frame.style.left = "0"; + frame.style.bottom = ""; + frame.style.width = this.width + "px"; frame.style.height = this.height + "px"; this.props.width = this.body.domProps.left.width; this.props.height = this.body.domProps.left.height; - } - else { // right - frame.style.top = ''; - frame.style.bottom = '0'; - frame.style.left = '0'; - frame.style.width = this.width + 'px'; + } else { + // right + frame.style.top = ""; + frame.style.bottom = "0"; + frame.style.left = "0"; + frame.style.width = this.width + "px"; frame.style.height = this.height + "px"; this.props.width = this.body.domProps.right.width; this.props.height = this.body.domProps.right.height; @@ -21440,8 +21273,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.icons == true) { this._redrawGroupIcons(); - } - else { + } else { this._cleanupIcons(); } @@ -21459,24 +21291,18 @@ return /******/ (function(modules) { // webpackBootstrap DOMutil.prepareElements(this.DOMelements.lines); DOMutil.prepareElements(this.DOMelements.labels); - var orientation = this.options['orientation']; + var orientation = this.options.orientation; // calculate range and step (step such that we have space for 7 characters per label) var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced; - var step = new DataStep( - this.range.start, - this.range.end, - minimumStep, - this.dom.frame.offsetHeight, - this.options.customRange[this.options.orientation], - this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on + var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation], this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on ); this.step = step; // get the distance in pixels for a step // dead space is space that is "left over" after a step - var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step)); + var stepPixels = (this.dom.frame.offsetHeight - step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange)) / ((step.marginRange - step.deadSpace) / step.step); this.stepPixels = stepPixels; @@ -21486,23 +21312,25 @@ return /******/ (function(modules) { // webpackBootstrap // the slave axis needs to use the same horizontal lines as the master axis. if (this.master == false) { stepPixels = this.stepPixelsForced; - stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps); + stepDifference = Math.round(this.dom.frame.offsetHeight / stepPixels - amountOfSteps); for (var i = 0; i < 0.5 * stepDifference; i++) { step.previous(); } amountOfSteps = this.height / stepPixels; if (this.zeroCrossing != -1 && this.options.alignZeros == true) { - var zeroStepDifference = (step.marginEnd / step.step) - this.zeroCrossing; + var zeroStepDifference = step.marginEnd / step.step - this.zeroCrossing; if (zeroStepDifference > 0) { - for (var i = 0; i < zeroStepDifference; i++) {step.next();} - } - else if (zeroStepDifference < 0) { - for (var i = 0; i < -zeroStepDifference; i++) {step.previous();} + for (var i = 0; i < zeroStepDifference; i++) { + step.next(); + } + } else if (zeroStepDifference < 0) { + for (var i = 0; i < -zeroStepDifference; i++) { + step.previous(); + } } } - } - else { + } else { amountOfSteps += 0.25; } @@ -21515,7 +21343,7 @@ return /******/ (function(modules) { // webpackBootstrap // Get the number of decimal places var decimals; - if(this.options.format[orientation] !== undefined) { + if (this.options.format[orientation] !== undefined) { decimals = this.options.format[orientation].decimals; } @@ -21527,19 +21355,17 @@ return /******/ (function(modules) { // webpackBootstrap marginStartPos = max * stepPixels; var isMajor = step.isMajor(); - if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) { - this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, 'yAxis minor', this.props.minorCharHeight); + if (this.options.showMinorLabels && isMajor == false || this.master == false && this.options.showMinorLabels == true) { + this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, "yAxis minor", this.props.minorCharHeight); } - if (isMajor && this.options['showMajorLabels'] && this.master == true || - this.options['showMinorLabels'] == false && this.master == false && isMajor == true) { + if (isMajor && this.options.showMajorLabels && this.master == true || this.options.showMinorLabels == false && this.master == false && isMajor == true) { if (y >= 0) { - this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, 'yAxis major', this.props.majorCharHeight); + this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, "yAxis major", this.props.majorCharHeight); } - this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth); - } - else { - this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth); + this._redrawLine(y, orientation, "grid horizontal major", this.options.majorLinesOffset, this.props.majorLineWidth); + } else { + this._redrawLine(y, orientation, "grid horizontal minor", this.options.minorLinesOffset, this.props.minorLineWidth); } if (this.master == true && step.current == 0) { @@ -21551,8 +21377,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.master == false) { this.conversionFactor = y / (this.valueAtZero - step.current); - } - else { + } else { this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; } @@ -21564,7 +21389,7 @@ return /******/ (function(modules) { // webpackBootstrap var offset = this.options.icons == true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15; // this will resize the yAxis to accommodate the labels. - if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) { + if (this.maxLabelSize > this.width - offset && this.options.visible == true) { this.width = this.maxLabelSize + offset; this.options.width = this.width + "px"; DOMutil.cleanupElements(this.DOMelements.lines); @@ -21573,15 +21398,14 @@ return /******/ (function(modules) { // webpackBootstrap resized = true; } // this will resize the yAxis if it is too big for the labels. - else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) { - this.width = Math.max(this.minWidth,this.maxLabelSize + offset); + else if (this.maxLabelSize < this.width - offset && this.options.visible == true && this.width > this.minWidth) { + this.width = Math.max(this.minWidth, this.maxLabelSize + offset); this.options.width = this.width + "px"; DOMutil.cleanupElements(this.DOMelements.lines); DOMutil.cleanupElements(this.DOMelements.labels); this.redraw(); resized = true; - } - else { + } else { DOMutil.cleanupElements(this.DOMelements.lines); DOMutil.cleanupElements(this.DOMelements.labels); resized = false; @@ -21607,23 +21431,22 @@ return /******/ (function(modules) { // webpackBootstrap */ DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) { // reuse redundant label - var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift(); + var label = DOMutil.getDOMElement("div", this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift(); label.className = className; label.innerHTML = text; - if (orientation == 'left') { - label.style.left = '-' + this.options.labelOffsetX + 'px'; + if (orientation == "left") { + label.style.left = "-" + this.options.labelOffsetX + "px"; label.style.textAlign = "right"; - } - else { - label.style.right = '-' + this.options.labelOffsetX + 'px'; + } else { + label.style.right = "-" + this.options.labelOffsetX + "px"; label.style.textAlign = "left"; } - label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px'; + label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; - text += ''; + text += ""; - var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth); + var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); if (this.maxLabelSize < text.length * largestWidth) { this.maxLabelSize = text.length * largestWidth; } @@ -21639,19 +21462,18 @@ return /******/ (function(modules) { // webpackBootstrap */ DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) { if (this.master == true) { - var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift(); + var line = DOMutil.getDOMElement("div", this.DOMelements.lines, this.dom.lineContainer); //this.dom.redundant.lines.shift(); line.className = className; - line.innerHTML = ''; + line.innerHTML = ""; - if (orientation == 'left') { - line.style.left = (this.width - offset) + 'px'; - } - else { - line.style.right = (this.width - offset) + 'px'; + if (orientation == "left") { + line.style.left = this.width - offset + "px"; + } else { + line.style.right = this.width - offset + "px"; } - line.style.width = width + 'px'; - line.style.top = y + 'px'; + line.style.width = width + "px"; + line.style.top = y + "px"; } }; @@ -21665,8 +21487,8 @@ return /******/ (function(modules) { // webpackBootstrap // Check if the title is defined for this axes if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) { - var title = DOMutil.getDOMElement('div', this.DOMelements.title, this.dom.frame); - title.className = 'yAxis title ' + orientation; + var title = DOMutil.getDOMElement("div", this.DOMelements.title, this.dom.frame); + title.className = "yAxis title " + orientation; title.innerHTML = this.options.title[orientation].text; // Add style - if provided @@ -21674,14 +21496,13 @@ return /******/ (function(modules) { // webpackBootstrap util.addCssText(title, this.options.title[orientation].style); } - if (orientation == 'left') { - title.style.left = this.props.titleCharHeight + 'px'; - } - else { - title.style.right = this.props.titleCharHeight + 'px'; + if (orientation == "left") { + title.style.left = this.props.titleCharHeight + "px"; + } else { + title.style.right = this.props.titleCharHeight + "px"; } - title.style.width = this.height + 'px'; + title.style.width = this.height + "px"; } // we need to clean up in case we did not use all elements. @@ -21698,10 +21519,10 @@ return /******/ (function(modules) { // webpackBootstrap */ DataAxis.prototype._calculateCharSize = function () { // determine the char width and height on the minor axis - if (!('minorCharHeight' in this.props)) { - var textMinor = document.createTextNode('0'); - var measureCharMinor = document.createElement('div'); - measureCharMinor.className = 'yAxis minor measure'; + if (!("minorCharHeight" in this.props)) { + var textMinor = document.createTextNode("0"); + var measureCharMinor = document.createElement("div"); + measureCharMinor.className = "yAxis minor measure"; measureCharMinor.appendChild(textMinor); this.dom.frame.appendChild(measureCharMinor); @@ -21711,10 +21532,10 @@ return /******/ (function(modules) { // webpackBootstrap this.dom.frame.removeChild(measureCharMinor); } - if (!('majorCharHeight' in this.props)) { - var textMajor = document.createTextNode('0'); - var measureCharMajor = document.createElement('div'); - measureCharMajor.className = 'yAxis major measure'; + if (!("majorCharHeight" in this.props)) { + var textMajor = document.createTextNode("0"); + var measureCharMajor = document.createElement("div"); + measureCharMajor.className = "yAxis major measure"; measureCharMajor.appendChild(textMajor); this.dom.frame.appendChild(measureCharMajor); @@ -21724,10 +21545,10 @@ return /******/ (function(modules) { // webpackBootstrap this.dom.frame.removeChild(measureCharMajor); } - if (!('titleCharHeight' in this.props)) { - var textTitle = document.createTextNode('0'); - var measureCharTitle = document.createElement('div'); - measureCharTitle.className = 'yAxis title measure'; + if (!("titleCharHeight" in this.props)) { + var textTitle = document.createTextNode("0"); + var measureCharTitle = document.createElement("div"); + measureCharTitle.className = "yAxis title measure"; measureCharTitle.appendChild(textTitle); this.dom.frame.appendChild(measureCharTitle); @@ -21740,11 +21561,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = DataAxis; - /***/ }, /* 45 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * @constructor DataStep * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an @@ -21784,8 +21606,8 @@ return /******/ (function(modules) { // webpackBootstrap this.marginEnd; this.deadSpace = 0; - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; this.alignZeros = alignZeros; @@ -21804,7 +21626,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} [end] The end date and time. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) { + DataStep.prototype.setRange = function (start, end, minimumStep, containerHeight, customRange) { this._start = customRange.min === undefined ? start : customRange.min; this._end = customRange.max === undefined ? end : customRange.max; @@ -21824,15 +21646,15 @@ return /******/ (function(modules) { // webpackBootstrap * Automatically determine the scale that bests fits the provided minimum step * @param {Number} [minimumStep] The minimum step size in milliseconds */ - DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) { + DataStep.prototype.setMinimumStep = function (minimumStep, containerHeight) { // round to floor var size = this._end - this._start; var safeSize = size * 1.2; var minimumStepValue = minimumStep * (safeSize / containerHeight); - var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10); + var orderOfMagnitude = Math.round(Math.log(safeSize) / Math.LN10); var minorStepIdx = -1; - var magnitudefactor = Math.pow(10,orderOfMagnitude); + var magnitudefactor = Math.pow(10, orderOfMagnitude); var start = 0; if (orderOfMagnitude < 0) { @@ -21841,7 +21663,7 @@ return /******/ (function(modules) { // webpackBootstrap var solutionFound = false; for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { - magnitudefactor = Math.pow(10,i); + magnitudefactor = Math.pow(10, i); for (var j = 0; j < this.minorSteps.length; j++) { var stepSize = magnitudefactor * this.minorSteps[j]; if (stepSize >= minimumStepValue) { @@ -21865,13 +21687,13 @@ return /******/ (function(modules) { // webpackBootstrap * Round the current date to the first minor date value * This must be executed once when the current date is set to start Date */ - DataStep.prototype.setFirst = function(customRange) { + DataStep.prototype.setFirst = function (customRange) { if (customRange === undefined) { customRange = {}; } - var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min; - var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max; + var niceStart = customRange.min === undefined ? this._start - this.scale * 2 * this.minorSteps[this.stepIndex] : customRange.min; + var niceEnd = customRange.max === undefined ? this._end + this.scale * this.minorSteps[this.stepIndex] : customRange.max; this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; @@ -21888,15 +21710,14 @@ return /******/ (function(modules) { // webpackBootstrap this.current = this.marginEnd; }; - DataStep.prototype.roundToMinor = function(value) { - var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex])); + DataStep.prototype.roundToMinor = function (value) { + var rounded = value - value % (this.scale * this.minorSteps[this.stepIndex]); if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { - return rounded + (this.scale * this.minorSteps[this.stepIndex]); - } - else { + return rounded + this.scale * this.minorSteps[this.stepIndex]; + } else { return rounded; } - } + }; /** @@ -21904,13 +21725,13 @@ return /******/ (function(modules) { // webpackBootstrap * @return {boolean} true if the current date has not passed the end date */ DataStep.prototype.hasNext = function () { - return (this.current >= this.marginStart); + return this.current >= this.marginStart; }; /** * Do the next step */ - DataStep.prototype.next = function() { + DataStep.prototype.next = function () { var prev = this.current; this.current -= this.step; @@ -21923,7 +21744,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Do the next step */ - DataStep.prototype.previous = function() { + DataStep.prototype.previous = function () { this.current += this.step; this.marginEnd += this.step; this.marginRange = this.marginEnd - this.marginStart; @@ -21935,60 +21756,55 @@ return /******/ (function(modules) { // webpackBootstrap * Get the current datetime * @return {String} current The current date */ - DataStep.prototype.getCurrent = function(decimals) { + DataStep.prototype.getCurrent = function (decimals) { // prevent round-off errors when close to zero - var current = (Math.abs(this.current) < this.step / 2) ? 0 : this.current; - var toPrecision = '' + Number(current).toPrecision(5); + var current = Math.abs(this.current) < this.step / 2 ? 0 : this.current; + var toPrecision = "" + Number(current).toPrecision(5); // If decimals is specified, then limit or extend the string as required - if(decimals !== undefined && !isNaN(Number(decimals))) { + if (decimals !== undefined && !isNaN(Number(decimals))) { // If string includes exponent, then we need to add it to the end var exp = ""; var index = toPrecision.indexOf("e"); - if(index != -1) { + if (index != -1) { // Get the exponent exp = toPrecision.slice(index); // Remove the exponent in case we need to zero-extend toPrecision = toPrecision.slice(0, index); } index = Math.max(toPrecision.indexOf(","), toPrecision.indexOf(".")); - if(index === -1) { + if (index === -1) { // No decimal found - if we want decimals, then we need to add it - if(decimals !== 0) { - toPrecision += '.'; + if (decimals !== 0) { + toPrecision += "."; } // Calculate how long the string should be index = toPrecision.length + decimals; - } - else if(decimals !== 0) { + } else if (decimals !== 0) { // Calculate how long the string should be - accounting for the decimal place index += decimals + 1; } - if(index > toPrecision.length) { + if (index > toPrecision.length) { // We need to add zeros! - for(var cnt = index - toPrecision.length; cnt > 0; cnt--) { - toPrecision += '0'; + for (var cnt = index - toPrecision.length; cnt > 0; cnt--) { + toPrecision += "0"; } - } - else { + } else { // we need to remove characters toPrecision = toPrecision.slice(0, index); } // Add the exponent if there is one toPrecision += exp; - } - else { + } else { if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) { // If no decimal is specified, and there are decimal places, remove trailing zeros for (var i = toPrecision.length - 1; i > 0; i--) { if (toPrecision[i] == "0") { toPrecision = toPrecision.slice(0, i); - } - else if (toPrecision[i] == "." || toPrecision[i] == ",") { + } else if (toPrecision[i] == "." || toPrecision[i] == ",") { toPrecision = toPrecision.slice(0, i); - break; - } - else { + break; + } else { break; } } @@ -22003,17 +21819,18 @@ return /******/ (function(modules) { // webpackBootstrap * is DAY, a major value is each first day of the MONTH) * @return {boolean} true if current date is major, else false. */ - DataStep.prototype.isMajor = function() { - return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0); + DataStep.prototype.isMajor = function () { + return this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0; }; module.exports = DataStep; - /***/ }, /* 46 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); var Line = __webpack_require__(47); @@ -22030,10 +21847,10 @@ return /******/ (function(modules) { // webpackBootstrap * It enumerates through the default styles * @constructor */ - function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) { + function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { this.id = groupId; - var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom'] - this.options = util.selectiveBridgeObject(fields,options); + var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart", "drawPoints", "shaded", "catmullRom"]; + this.options = util.selectiveBridgeObject(fields, options); this.usingDefaultStyle = group.className === undefined; this.groupsUsingDefaultStyles = groupsUsingDefaultStyles; this.zeroPosition = 0; @@ -22050,14 +21867,15 @@ return /******/ (function(modules) { // webpackBootstrap * this loads a reference to all items in this group into this group. * @param {array} items */ - GraphGroup.prototype.setItems = function(items) { + GraphGroup.prototype.setItems = function (items) { if (items != null) { this.itemsData = items; if (this.options.sort == true) { - this.itemsData.sort(function (a,b) {return a.x - b.x;}) + this.itemsData.sort(function (a, b) { + return a.x - b.x; + }); } - } - else { + } else { this.itemsData = []; } }; @@ -22067,7 +21885,7 @@ return /******/ (function(modules) { // webpackBootstrap * this is used for plotting barcharts, this way, we only have to calculate it once. * @param pos */ - GraphGroup.prototype.setZeroPosition = function(pos) { + GraphGroup.prototype.setZeroPosition = function (pos) { this.zeroPosition = pos; }; @@ -22076,26 +21894,24 @@ return /******/ (function(modules) { // webpackBootstrap * set the options of the graph group over the default options. * @param options */ - GraphGroup.prototype.setOptions = function(options) { + GraphGroup.prototype.setOptions = function (options) { if (options !== undefined) { - var fields = ['sampling','style','sort','yAxisOrientation','barChart']; + var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart"]; util.selectiveDeepExtend(fields, this.options, options); - util.mergeOptions(this.options, options,'catmullRom'); - util.mergeOptions(this.options, options,'drawPoints'); - util.mergeOptions(this.options, options,'shaded'); + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); if (options.catmullRom) { - if (typeof options.catmullRom == 'object') { + if (typeof options.catmullRom == "object") { if (options.catmullRom.parametrization) { - if (options.catmullRom.parametrization == 'uniform') { + if (options.catmullRom.parametrization == "uniform") { this.options.catmullRom.alpha = 0; - } - else if (options.catmullRom.parametrization == 'chordal') { - this.options.catmullRom.alpha = 1.0; - } - else { - this.options.catmullRom.parametrization = 'centripetal'; + } else if (options.catmullRom.parametrization == "chordal") { + this.options.catmullRom.alpha = 1; + } else { + this.options.catmullRom.parametrization = "centripetal"; this.options.catmullRom.alpha = 0.5; } } @@ -22103,13 +21919,11 @@ return /******/ (function(modules) { // webpackBootstrap } } - if (this.options.style == 'line') { + if (this.options.style == "line") { this.type = new Line(this.id, this.options); - } - else if (this.options.style == 'bar') { + } else if (this.options.style == "bar") { this.type = new Bar(this.id, this.options); - } - else if (this.options.style == 'points') { + } else if (this.options.style == "points") { this.type = new Points(this.id, this.options); } }; @@ -22119,9 +21933,9 @@ return /******/ (function(modules) { // webpackBootstrap * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph * @param group */ - GraphGroup.prototype.update = function(group) { + GraphGroup.prototype.update = function (group) { this.group = group; - this.content = group.content || 'graph'; + this.content = group.content || "graph"; this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10; this.visible = group.visible === undefined ? true : group.visible; this.style = group.style; @@ -22139,7 +21953,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param iconWidth * @param iconHeight */ - GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { + GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { var fillHeight = iconHeight * 0.5; var path, fillPath; @@ -22147,45 +21961,39 @@ return /******/ (function(modules) { // webpackBootstrap outline.setAttributeNS(null, "x", x); outline.setAttributeNS(null, "y", y - fillHeight); outline.setAttributeNS(null, "width", iconWidth); - outline.setAttributeNS(null, "height", 2*fillHeight); + outline.setAttributeNS(null, "height", 2 * fillHeight); outline.setAttributeNS(null, "class", "outline"); - if (this.options.style == 'line') { + if (this.options.style == "line") { path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); path.setAttributeNS(null, "class", this.className); - if(this.style !== undefined) { + if (this.style !== undefined) { path.setAttributeNS(null, "style", this.style); } - path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+""); + path.setAttributeNS(null, "d", "M" + x + "," + y + " L" + (x + iconWidth) + "," + y + ""); if (this.options.shaded.enabled == true) { fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); - if (this.options.shaded.orientation == 'top') { - fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) + - "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight)); - } - else { - fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " + - "L"+x+"," + (y + fillHeight) + " " + - "L"+ (x + iconWidth) + "," + (y + fillHeight) + - "L"+ (x + iconWidth) + ","+y); + if (this.options.shaded.orientation == "top") { + fillPath.setAttributeNS(null, "d", "M" + x + ", " + (y - fillHeight) + "L" + x + "," + y + " L" + (x + iconWidth) + "," + y + " L" + (x + iconWidth) + "," + (y - fillHeight)); + } else { + fillPath.setAttributeNS(null, "d", "M" + x + "," + y + " " + "L" + x + "," + (y + fillHeight) + " " + "L" + (x + iconWidth) + "," + (y + fillHeight) + "L" + (x + iconWidth) + "," + y); } fillPath.setAttributeNS(null, "class", this.className + " iconFill"); } if (this.options.drawPoints.enabled == true) { - DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer); + DOMutil.drawPoint(x + 0.5 * iconWidth, y, this, JSONcontainer, SVGcontainer); } - } - else { + } else { var barWidth = Math.round(0.3 * iconWidth); var bar1Height = Math.round(0.4 * iconHeight); var bar2Height = Math.round(0.75 * iconHeight); - var offset = Math.round((iconWidth - (2 * barWidth))/3); + var offset = Math.round((iconWidth - 2 * barWidth) / 3); - DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer); - DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer); + DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + " bar", JSONcontainer, SVGcontainer); + DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + " bar", JSONcontainer, SVGcontainer); } }; @@ -22197,28 +22005,29 @@ return /******/ (function(modules) { // webpackBootstrap * @param iconHeight * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} */ - GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) { - var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg"); - this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight); - return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation}; - } + GraphGroup.prototype.getLegend = function (iconWidth, iconHeight) { + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.drawIcon(0, 0.5 * iconHeight, [], svg, iconWidth, iconHeight); + return { icon: svg, label: this.content, orientation: this.options.yAxisOrientation }; + }; - GraphGroup.prototype.getYRange = function(groupData) { + GraphGroup.prototype.getYRange = function (groupData) { return this.type.getYRange(groupData); - } + }; - GraphGroup.prototype.draw = function(dataset, group, framework) { + GraphGroup.prototype.draw = function (dataset, group, framework) { this.type.draw(dataset, group, framework); - } + }; module.exports = GraphGroup; - /***/ }, /* 47 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * Created by Alex on 11/11/2014. */ @@ -22230,14 +22039,14 @@ return /******/ (function(modules) { // webpackBootstrap this.options = options; } - Line.prototype.getYRange = function(groupData) { + Line.prototype.getYRange = function (groupData) { var yMin = groupData[0].y; var yMax = groupData[0].y; for (var j = 0; j < groupData.length; j++) { yMin = yMin > groupData[j].y ? groupData[j].y : yMin; yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } - return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation}; + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; @@ -22251,39 +22060,37 @@ return /******/ (function(modules) { // webpackBootstrap if (dataset != null) { if (dataset.length > 0) { var path, d; - var svgHeight = Number(framework.svg.style.height.replace('px','')); - path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); + var svgHeight = Number(framework.svg.style.height.replace("px", "")); + path = DOMutil.getSVGElement("path", framework.svgElements, framework.svg); path.setAttributeNS(null, "class", group.className); - if(group.style !== undefined) { + if (group.style !== undefined) { path.setAttributeNS(null, "style", group.style); } // construct path from dataset if (group.options.catmullRom.enabled == true) { d = Line._catmullRom(dataset, group); - } - else { + } else { d = Line._linear(dataset); } // append with points for fill and finalize the path if (group.options.shaded.enabled == true) { - var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); + var fillPath = DOMutil.getSVGElement("path", framework.svgElements, framework.svg); var dFill; - if (group.options.shaded.orientation == 'top') { - dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0; - } - else { - dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight; + if (group.options.shaded.orientation == "top") { + dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0; + } else { + dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight; } fillPath.setAttributeNS(null, "class", group.className + " fill"); - if(group.options.shaded.style !== undefined) { + if (group.options.shaded.style !== undefined) { fillPath.setAttributeNS(null, "style", group.options.shaded.style); } fillPath.setAttributeNS(null, "d", dFill); } // copy properties to path for drawing. - path.setAttributeNS(null, 'd', 'M' + d); + path.setAttributeNS(null, "d", "M" + d); // draw points if (group.options.drawPoints.enabled == true) { @@ -22302,18 +22109,17 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {string} * @private */ - Line._catmullRomUniform = function(data) { + Line._catmullRomUniform = function (data) { // catmull rom var p0, p1, p2, p3, bp1, bp2; - var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; - var normalization = 1/6; + var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; + var normalization = 1 / 6; var length = data.length; for (var i = 0; i < length - 1; i++) { - - p0 = (i == 0) ? data[0] : data[i-1]; + p0 = i == 0 ? data[0] : data[i - 1]; p1 = data[i]; - p2 = data[i+1]; - p3 = (i + 2 < length) ? data[i+2] : p2; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; // Catmull-Rom to Cubic Bezier conversion matrix @@ -22323,17 +22129,11 @@ return /******/ (function(modules) { // webpackBootstrap // 0 0 1 0 // bp0 = { x: p1.x, y: p1.y }; - bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)}; - bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)}; + bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; + bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; // bp0 = { x: p2.x, y: p2.y }; - d += 'C' + - bp1.x + ',' + - bp1.y + ' ' + - bp2.x + ',' + - bp2.y + ' ' + - p2.x + ',' + - p2.y + ' '; + d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; } return d; @@ -22350,26 +22150,24 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {string} * @private */ - Line._catmullRom = function(data, group) { + Line._catmullRom = function (data, group) { var alpha = group.options.catmullRom.alpha; if (alpha == 0 || alpha === undefined) { return this._catmullRomUniform(data); - } - else { - var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M; + } else { + var p0, p1, p2, p3, bp1, bp2, d1, d2, d3, A, B, N, M; var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA; - var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; + var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; var length = data.length; for (var i = 0; i < length - 1; i++) { - - p0 = (i == 0) ? data[0] : data[i-1]; + p0 = i == 0 ? data[0] : data[i - 1]; p1 = data[i]; - p2 = data[i+1]; - p3 = (i + 2 < length) ? data[i+2] : p2; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; - d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2)); - d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2)); - d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2)); + d1 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); + d2 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); + d3 = Math.sqrt(Math.pow(p2.x - p3.x, 2) + Math.pow(p2.y - p3.y, 2)); // Catmull-Rom to Cubic Bezier conversion matrix @@ -22381,35 +22179,37 @@ return /******/ (function(modules) { // webpackBootstrap // [ 0 d3^2a /M B/M -d2^2a /M ] // [ 0 0 1 0 ] - d3powA = Math.pow(d3, alpha); - d3pow2A = Math.pow(d3,2*alpha); - d2powA = Math.pow(d2, alpha); - d2pow2A = Math.pow(d2,2*alpha); - d1powA = Math.pow(d1, alpha); - d1pow2A = Math.pow(d1,2*alpha); - - A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A; - B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A; - N = 3*d1powA * (d1powA + d2powA); - if (N > 0) {N = 1 / N;} - M = 3*d3powA * (d3powA + d2powA); - if (M > 0) {M = 1 / M;} - - bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N), - y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)}; - - bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M), - y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)}; - - if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;} - if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;} - d += 'C' + - bp1.x + ',' + - bp1.y + ' ' + - bp2.x + ',' + - bp2.y + ' ' + - p2.x + ',' + - p2.y + ' '; + d3powA = Math.pow(d3, alpha); + d3pow2A = Math.pow(d3, 2 * alpha); + d2powA = Math.pow(d2, alpha); + d2pow2A = Math.pow(d2, 2 * alpha); + d1powA = Math.pow(d1, alpha); + d1pow2A = Math.pow(d1, 2 * alpha); + + A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A; + B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A; + N = 3 * d1powA * (d1powA + d2powA); + if (N > 0) { + N = 1 / N; + } + M = 3 * d3powA * (d3powA + d2powA); + if (M > 0) { + M = 1 / M; + } + + bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, + y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + + bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, + y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + + if (bp1.x == 0 && bp1.y == 0) { + bp1 = p1; + } + if (bp2.x == 0 && bp2.y == 0) { + bp2 = p2; + } + d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; } return d; @@ -22422,15 +22222,14 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {string} * @private */ - Line._linear = function(data) { + Line._linear = function (data) { // linear - var d = ''; + var d = ""; for (var i = 0; i < data.length; i++) { if (i == 0) { - d += data[i].x + ',' + data[i].y; - } - else { - d += ' ' + data[i].x + ',' + data[i].y; + d += data[i].x + "," + data[i].y; + } else { + d += " " + data[i].x + "," + data[i].y; } } return d; @@ -22438,11 +22237,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Line; - /***/ }, /* 48 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * Created by Alex on 11/11/2014. */ @@ -22454,19 +22254,19 @@ return /******/ (function(modules) { // webpackBootstrap } - Points.prototype.getYRange = function(groupData) { + Points.prototype.getYRange = function (groupData) { var yMin = groupData[0].y; var yMax = groupData[0].y; for (var j = 0; j < groupData.length; j++) { yMin = yMin > groupData[j].y ? groupData[j].y : yMin; yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } - return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation}; + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; - Points.prototype.draw = function(dataset, group, framework, offset) { + Points.prototype.draw = function (dataset, group, framework, offset) { Points.draw(dataset, group, framework, offset); - } + }; /** * draw the data points @@ -22478,7 +22278,9 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} [offset] */ Points.draw = function (dataset, group, framework, offset) { - if (offset === undefined) {offset = 0;} + if (offset === undefined) { + offset = 0; + } for (var i = 0; i < dataset.length; i++) { DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label); } @@ -22491,6 +22293,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 49 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * Created by Alex on 11/11/2014. */ @@ -22502,17 +22306,16 @@ return /******/ (function(modules) { // webpackBootstrap this.options = options; } - Bargraph.prototype.getYRange = function(groupData) { - if (this.options.barChart.handleOverlap != 'stack') { + Bargraph.prototype.getYRange = function (groupData) { + if (this.options.barChart.handleOverlap != "stack") { var yMin = groupData[0].y; var yMax = groupData[0].y; for (var j = 0; j < groupData.length; j++) { yMin = yMin > groupData[j].y ? groupData[j].y : yMin; yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } - return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation}; - } - else { + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + } else { var barCombinedData = []; for (var j = 0; j < groupData.length; j++) { barCombinedData.push({ @@ -22539,19 +22342,20 @@ return /******/ (function(modules) { // webpackBootstrap var coreDistance; var key, drawData; var group; - var i,j; + var i, j; var barPoints = 0; // combine all barchart data for (i = 0; i < groupIds.length; i++) { group = framework.groups[groupIds[i]]; - if (group.options.style == 'bar') { + if (group.options.style == "bar") { if (group.visible == true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] == true)) { for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { combinedData.push({ x: processedGroupData[groupIds[i]][j].x, y: processedGroupData[groupIds[i]][j].y, - groupId: groupIds[i] + groupId: groupIds[i], + label: processedGroupData[groupIds[i]][j].label }); barPoints += 1; } @@ -22559,7 +22363,9 @@ return /******/ (function(modules) { // webpackBootstrap } } - if (barPoints == 0) {return;} + if (barPoints == 0) { + return; + } // sort by time and by group combinedData.sort(function (a, b) { @@ -22581,33 +22387,43 @@ return /******/ (function(modules) { // webpackBootstrap key = combinedData[i].x; var heightOffset = 0; if (intersections[key] === undefined) { - if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);} - if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));} + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - key); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); + } drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - } - else { + } else { var nextKey = i + (intersections[key].amount - intersections[key].resolved); var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);} - if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));} + if (nextKey < combinedData.length) { + coreDistance = Math.abs(combinedData[nextKey].x - key); + } + if (prevKey > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + } drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); intersections[key].resolved += 1; - if (group.options.barChart.handleOverlap == 'stack') { + if (group.options.barChart.handleOverlap == "stack") { heightOffset = intersections[key].accumulated; intersections[key].accumulated += group.zeroPosition - combinedData[i].y; - } - else if (group.options.barChart.handleOverlap == 'sideBySide') { + } else if (group.options.barChart.handleOverlap == "sideBySide") { drawData.width = drawData.width / intersections[key].amount; - drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1)); - if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;} - else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;} + drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); + if (group.options.barChart.align == "left") { + drawData.offset -= 0.5 * drawData.width; + } else if (group.options.barChart.align == "right") { + drawData.offset += 0.5 * drawData.width; + } } } - DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg); + DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + " bar", framework.svgElements, framework.svg); // draw points if (group.options.drawPoints.enabled == true) { - DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); + Points.draw([combinedData[i]], group, framework, drawData.offset); + //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); } } }; @@ -22631,7 +22447,7 @@ return /******/ (function(modules) { // webpackBootstrap } if (coreDistance == 0) { if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0}; + intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulated: 0 }; } intersections[combinedData[i].x].amount += 1; } @@ -22654,29 +22470,26 @@ return /******/ (function(modules) { // webpackBootstrap width = coreDistance < minWidth ? minWidth : coreDistance; offset = 0; // recalculate offset with the new width; - if (group.options.barChart.align == 'left') { + if (group.options.barChart.align == "left") { offset -= 0.5 * coreDistance; - } - else if (group.options.barChart.align == 'right') { + } else if (group.options.barChart.align == "right") { offset += 0.5 * coreDistance; } - } - else { + } else { // default settings width = group.options.barChart.width; offset = 0; - if (group.options.barChart.align == 'left') { + if (group.options.barChart.align == "left") { offset -= 0.5 * group.options.barChart.width; - } - else if (group.options.barChart.align == 'right') { + } else if (group.options.barChart.align == "right") { offset += 0.5 * group.options.barChart.width; } } - return {width: width, offset: offset}; + return { width: width, offset: offset }; }; - Bargraph.getStackedBarYRange = function(barCombinedData, groupRanges, groupIds, groupLabel, orientation) { + Bargraph.getStackedBarYRange = function (barCombinedData, groupRanges, groupIds, groupLabel, orientation) { if (barCombinedData.length > 0) { // sort by time and by group barCombinedData.sort(function (a, b) { @@ -22693,7 +22506,7 @@ return /******/ (function(modules) { // webpackBootstrap groupRanges[groupLabel].yAxisOrientation = orientation; groupIds.push(groupLabel); } - } + }; Bargraph._getStackedBarYRange = function (intersections, combinedData) { var key; @@ -22704,8 +22517,7 @@ return /******/ (function(modules) { // webpackBootstrap if (intersections[key] === undefined) { yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; - } - else { + } else { intersections[key].accumulated += combinedData[i].y; } } @@ -22716,7 +22528,7 @@ return /******/ (function(modules) { // webpackBootstrap } } - return {min: yMin, max: yMax}; + return { min: yMin, max: yMax }; }; module.exports = Bargraph; @@ -22725,6 +22537,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 50 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); var Component = __webpack_require__(23); @@ -22741,15 +22555,15 @@ return /******/ (function(modules) { // webpackBootstrap iconSpacing: 6, left: { visible: true, - position: 'top-left' // top/bottom - left,center,right + position: "top-left" // top/bottom - left,center,right }, right: { visible: true, - position: 'top-left' // top/bottom - left,center,right + position: "top-left" // top/bottom - left,center,right } - } + }; this.side = side; - this.options = util.extend({},this.defaultOptions); + this.options = util.extend({}, this.defaultOptions); this.linegraphOptions = linegraphOptions; this.svgElements = {}; @@ -22763,47 +22577,46 @@ return /******/ (function(modules) { // webpackBootstrap Legend.prototype = new Component(); - Legend.prototype.clear = function() { + Legend.prototype.clear = function () { this.groups = {}; this.amountOfGroups = 0; - } - - Legend.prototype.addGroup = function(label, graphOptions) { + }; + Legend.prototype.addGroup = function (label, graphOptions) { if (!this.groups.hasOwnProperty(label)) { this.groups[label] = graphOptions; } this.amountOfGroups += 1; }; - Legend.prototype.updateGroup = function(label, graphOptions) { + Legend.prototype.updateGroup = function (label, graphOptions) { this.groups[label] = graphOptions; }; - Legend.prototype.removeGroup = function(label) { + Legend.prototype.removeGroup = function (label) { if (this.groups.hasOwnProperty(label)) { delete this.groups[label]; this.amountOfGroups -= 1; } }; - Legend.prototype._create = function() { - this.dom.frame = document.createElement('div'); - this.dom.frame.className = 'legend'; + Legend.prototype._create = function () { + this.dom.frame = document.createElement("div"); + this.dom.frame.className = "legend"; this.dom.frame.style.position = "absolute"; this.dom.frame.style.top = "10px"; this.dom.frame.style.display = "block"; - this.dom.textArea = document.createElement('div'); - this.dom.textArea.className = 'legendText'; + this.dom.textArea = document.createElement("div"); + this.dom.textArea.className = "legendText"; this.dom.textArea.style.position = "relative"; this.dom.textArea.style.top = "0px"; - this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg"); - this.svg.style.position = 'absolute'; - this.svg.style.top = 0 +'px'; - this.svg.style.width = this.options.iconSize + 5 + 'px'; - this.svg.style.height = '100%'; + this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.svg.style.position = "absolute"; + this.svg.style.top = 0 + "px"; + this.svg.style.width = this.options.iconSize + 5 + "px"; + this.svg.style.height = "100%"; this.dom.frame.appendChild(this.svg); this.dom.frame.appendChild(this.dom.textArea); @@ -22812,7 +22625,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the component from the DOM */ - Legend.prototype.hide = function() { + Legend.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); @@ -22823,19 +22636,19 @@ return /******/ (function(modules) { // webpackBootstrap * Show the component in the DOM (when not already visible). * @return {Boolean} changed */ - Legend.prototype.show = function() { + Legend.prototype.show = function () { // show frame containing the items if (!this.dom.frame.parentNode) { this.body.dom.center.appendChild(this.dom.frame); } }; - Legend.prototype.setOptions = function(options) { - var fields = ['enabled','orientation','icons','left','right']; + Legend.prototype.setOptions = function (options) { + var fields = ["enabled", "orientation", "icons", "left", "right"]; util.selectiveDeepExtend(fields, this.options, options); }; - Legend.prototype.redraw = function() { + Legend.prototype.redraw = function () { var activeGroups = 0; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { @@ -22847,73 +22660,69 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) { this.hide(); - } - else { + } else { this.show(); - if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') { - this.dom.frame.style.left = '4px'; + if (this.options[this.side].position == "top-left" || this.options[this.side].position == "bottom-left") { + this.dom.frame.style.left = "4px"; this.dom.frame.style.textAlign = "left"; this.dom.textArea.style.textAlign = "left"; - this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px'; - this.dom.textArea.style.right = ''; - this.svg.style.left = 0 +'px'; - this.svg.style.right = ''; - } - else { - this.dom.frame.style.right = '4px'; + this.dom.textArea.style.left = this.options.iconSize + 15 + "px"; + this.dom.textArea.style.right = ""; + this.svg.style.left = 0 + "px"; + this.svg.style.right = ""; + } else { + this.dom.frame.style.right = "4px"; this.dom.frame.style.textAlign = "right"; this.dom.textArea.style.textAlign = "right"; - this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px'; - this.dom.textArea.style.left = ''; - this.svg.style.right = 0 +'px'; - this.svg.style.left = ''; + this.dom.textArea.style.right = this.options.iconSize + 15 + "px"; + this.dom.textArea.style.left = ""; + this.svg.style.right = 0 + "px"; + this.svg.style.left = ""; } - if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') { - this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px'; - this.dom.frame.style.bottom = ''; - } - else { + if (this.options[this.side].position == "top-left" || this.options[this.side].position == "top-right") { + this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px", "")) + "px"; + this.dom.frame.style.bottom = ""; + } else { var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; - this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px","")) + 'px'; - this.dom.frame.style.top = ''; + this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px", "")) + "px"; + this.dom.frame.style.top = ""; } if (this.options.icons == false) { - this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px'; - this.dom.textArea.style.right = ''; - this.dom.textArea.style.left = ''; - this.svg.style.width = '0px'; - } - else { - this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px' + this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + "px"; + this.dom.textArea.style.right = ""; + this.dom.textArea.style.left = ""; + this.svg.style.width = "0px"; + } else { + this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + "px"; this.drawLegendIcons(); } - var content = ''; + var content = ""; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - content += this.groups[groupId].content + '
'; + content += this.groups[groupId].content + "
"; } } } this.dom.textArea.innerHTML = content; - this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px'; + this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; } }; - Legend.prototype.drawLegendIcons = function() { + Legend.prototype.drawLegendIcons = function () { if (this.dom.frame.parentNode) { DOMutil.prepareElements(this.svgElements); var padding = window.getComputedStyle(this.dom.frame).paddingTop; - var iconOffset = Number(padding.replace('px','')); + var iconOffset = Number(padding.replace("px", "")); var x = iconOffset; var iconWidth = this.options.iconSize; var iconHeight = 0.75 * this.options.iconSize; var y = iconOffset + 0.5 * iconHeight + 3; - this.svg.style.width = iconWidth + 5 + iconOffset + 'px'; + this.svg.style.width = iconWidth + 5 + iconOffset + "px"; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { @@ -22930,11 +22739,12 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Legend; - /***/ }, /* 51 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + var Emitter = __webpack_require__(11); var Hammer = __webpack_require__(19); var keycharm = __webpack_require__(37); @@ -22951,10 +22761,18 @@ return /******/ (function(modules) { // webpackBootstrap var Popup = __webpack_require__(58); var MixinLoader = __webpack_require__(59); var Activator = __webpack_require__(36); - var locales = __webpack_require__(70); + var locales = __webpack_require__(64); // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(71); + __webpack_require__(65); + + var PhysicsEngine = __webpack_require__(66).PhysicsEngine; + var ClusterEngine = __webpack_require__(73).ClusterEngine; + var CanvasRenderer = __webpack_require__(74).CanvasRenderer; + var Canvas = __webpack_require__(75).Canvas; + var View = __webpack_require__(76).View; + var TouchEventHandler = __webpack_require__(77).TouchEventHandler; + /** * @constructor Network @@ -22967,38 +22785,28 @@ return /******/ (function(modules) { // webpackBootstrap * {Array} edges * @param {Object} options Options */ - function Network (container, data, options) { + function Network(container, data, options) { if (!(this instanceof Network)) { - throw new SyntaxError('Constructor must be called with the new operator'); + throw new SyntaxError("Constructor must be called with the new operator"); } - this._determineBrowserMethod(); this._initializeMixinLoaders(); - // create variables and set default values - this.containerElement = container; // render and calculation settings - this.renderRefreshRate = 60; // hz (fps) - this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on - this.renderTime = 0; // measured time it takes to render a frame - this.physicsTime = 0; // measured time it takes to render a frame - this.runDoubleSpeed = false; - this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation - this.initializing = true; - this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; + this.triggerFunctions = { add: null, edit: null, editEdge: null, connect: null, del: null }; - var customScalingFunction = function (min,max,total,value) { + var customScalingFunction = function (min, max, total, value) { if (max == min) { return 0.5; - } - else { + } else { var scale = 1 / (max - min); - return Math.max(0,(value - min)*scale); + return Math.max(0, (value - min) * scale); } }; + // set constant values this.defaultOptions = { nodes: { @@ -23007,32 +22815,33 @@ return /******/ (function(modules) { // webpackBootstrap radiusMin: 10, radiusMax: 30, radius: 10, - shape: 'ellipse', + shape: "ellipse", image: undefined, widthMin: 16, // px widthMax: 64, // px - fontColor: 'black', + fontColor: "black", fontSize: 14, // px - fontFace: 'verdana', + fontFace: "verdana", fontFill: undefined, fontStrokeWidth: 0, // px - fontStrokeColor: '#ffffff', + fontStrokeColor: "#ffffff", fontDrawThreshold: 3, scaleFontWithValue: false, fontSizeMin: 14, fontSizeMax: 30, fontSizeMaxVisible: 30, + value: 1, level: -1, color: { - border: '#2B7CE9', - background: '#97C2FC', + border: "#2B7CE9", + background: "#97C2FC", highlight: { - border: '#2B7CE9', - background: '#D2E5FF' + border: "#2B7CE9", + background: "#D2E5FF" }, hover: { - border: '#2B7CE9', - background: '#D2E5FF' + border: "#2B7CE9", + background: "#D2E5FF" } }, group: undefined, @@ -23042,24 +22851,25 @@ return /******/ (function(modules) { // webpackBootstrap edges: { customScalingFunction: customScalingFunction, widthMin: 1, // - widthMax: 15,// + widthMax: 15, // width: 1, widthSelectionMultiplier: 2, hoverWidth: 1.5, - style: 'line', + value: 1, + style: "line", color: { - color:'#848484', - highlight:'#848484', - hover: '#848484' + color: "#848484", + highlight: "#848484", + hover: "#848484" }, - opacity:1.0, - fontColor: '#343434', + opacity: 1, + fontColor: "#343434", fontSize: 14, // px - fontFace: 'arial', - fontFill: 'white', + fontFace: "arial", + fontFill: "white", fontStrokeWidth: 0, // px - fontStrokeColor: 'white', - labelAlignment:'horizontal', + fontStrokeColor: "white", + labelAlignment: "horizontal", arrowScaleFactor: 1, dash: { length: 10, @@ -23069,65 +22879,13 @@ return /******/ (function(modules) { // webpackBootstrap inheritColor: "from", // to, from, false, true (== from) useGradients: false // release in 4.0 }, - configurePhysics:false, - physics: { - barnesHut: { - enabled: true, - thetaInverted: 1 / 0.5, // inverted to save time during calculation - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0.0, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - enabled: false, - centralGravity: 0.0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 150, - damping: 0.09 - }, - damping: null, - centralGravity: null, - springLength: null, - springConstant: null - }, - clustering: { // Per Node in Cluster = PNiC - enabled: false, // (Boolean) | global on/off switch for clustering. - initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. - clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes - reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this - chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). - clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. - sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. - screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. - fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). - maxFontSize: 1000, - forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). - distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). - edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. - nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster. - height: 1, // (px PNiC) | growth of the height per node in cluster. - radius: 1}, // (px PNiC) | growth of the radius per node in cluster. - maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster. - activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open. - clusterLevelDifference: 2, // used for normalization of the cluster levels - clusterByZoom: true // enable clustering through zooming in and out - }, + configurePhysics: false, navigation: { enabled: false }, keyboard: { enabled: false, - speed: {x: 10, y: 10, zoom: 0.02}, + speed: { x: 10, y: 10, zoom: 0.02 }, bindToWindow: true }, dataManipulation: { @@ -23135,34 +22893,29 @@ return /******/ (function(modules) { // webpackBootstrap initiallyVisible: false }, hierarchicalLayout: { - enabled:false, + enabled: false, levelSeparation: 150, nodeSpacing: 100, - direction: "UD", // UD, DU, LR, RL + direction: "UD", // UD, DU, LR, RL layout: "hubsize" // hubsize, directed }, - freezeForStabilization: false, + smoothCurves: { enabled: true, dynamic: true, type: "continuous", roundness: 0.5 }, - maxVelocity: 50, - minVelocity: 0.1, // px/s - stabilize: true, // stabilize before displaying the network - stabilizationIterations: 1000, // maximum number of iteration to stabilize - zoomExtentOnStabilize: true, - locale: 'en', + locale: "en", locales: locales, tooltip: { delay: 300, - fontColor: 'black', + fontColor: "black", fontSize: 14, // px - fontFace: 'verdana', + fontFace: "verdana", color: { - border: '#666', - background: '#FFFFC6' + border: "#666", + background: "#FFFFC6" } }, dragNetwork: true, @@ -23171,40 +22924,73 @@ return /******/ (function(modules) { // webpackBootstrap hover: false, hideEdgesOnDrag: false, hideNodesOnDrag: false, - width : '100%', - height : '100%', + width: "100%", + height: "100%", selectable: true, useDefaultGroups: true }; this.constants = util.extend({}, this.defaultOptions); - this.pixelRatio = 1; - - - this.hoverObj = {nodes:{},edges:{}}; + + // containers for nodes and edges + this.body = { + nodes: {}, + nodeIndices: [], + supportNodes: {}, + supportNodeIndices: [], + edges: {}, + data: { + nodes: null, // A DataSet or DataView + edges: null // A DataSet or DataView + }, + functions: { + createNode: this._createNode.bind(this), + createEdge: this._createEdge.bind(this) + }, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this), + once: this.once.bind(this) + }, + eventListeners: { + onTap: function () {}, + onTouch: function () {}, + onDoubleTap: function () {}, + onHold: function () {}, + onDragStart: function () {}, + onDrag: function () {}, + onDragEnd: function () {}, + onMouseWheel: function () {}, + onPinch: function () {}, + onMouseMove: function () {}, + onRelease: function () {} + }, + container: container + }; + + // modules + this.view = new View(this.body); + this.renderer = new CanvasRenderer(this.body); + this.clustering = new ClusterEngine(this.body); + this.physics = new PhysicsEngine(this.body); + this.canvas = new Canvas(this.body); + this.touchHandler = new TouchEventHandler(this.body); + + this.renderer.setCanvas(this.canvas); + this.view.setCanvas(this.canvas); + this.touchHandler.setCanvas(this.canvas); + + this.hoverObj = { nodes: {}, edges: {} }; this.controlNodesActive = false; this.navigationHammers = []; this.manipulationHammers = []; - // animation properties - this.animationSpeed = 1/this.renderRefreshRate; - this.animationEasingFunction = "easeInOutQuint"; - this.animating = false; - this.easingTime = 0; - this.sourceScale = 0; - this.targetScale = 0; - this.sourceTranslation = 0; - this.targetTranslation = 0; - this.lockedOnNodeId = null; - this.lockedOnNodeOffset = null; - this.touchTime = 0; - this.redrawRequested = false; - // Node variables - var network = this; + var me = this; this.groups = new Groups(); // object with groups this.images = new Images(); // object with images this.images.setOnloadCallback(function (status) { - network._requestRedraw(); + me._requestRedraw(); }); // keyboard navigation variables @@ -23214,318 +23000,119 @@ return /******/ (function(modules) { // webpackBootstrap // loading all the mixins: // load the force calculation functions, grouped under the physics system. - this._loadPhysicsSystem(); + //this._loadPhysicsSystem(); // create a frame and canvas - this._create(); - // load the sector system. (mandatory, fully integrated with Network) - this._loadSectorSystem(); // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) - this._loadClusterSystem(); // load the selection system. (mandatory, required by Network) this._loadSelectionSystem(); // load the selection system. (mandatory, required by Network) - this._loadHierarchySystem(); - + //this._loadHierarchySystem(); // apply options - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); this.setOptions(options); // other vars - this.freezeSimulationEnabled = false;// freeze the simulation this.cachedFunctions = {}; this.startedStabilization = false; this.stabilized = false; this.stabilizationIterations = null; this.draggingNodes = false; - // containers for nodes and edges - this.calculationNodes = {}; - this.calculationNodeIndices = []; - this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects - // position and scale variables and objects - this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. - this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action - this.scale = 1; // defining the global scale variable in the constructor - this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out - // datasets or dataviews - this.nodesData = null; // A DataSet or DataView - this.edgesData = null; // A DataSet or DataView + this.pointerPosition = { x: 0, y: 0 }; // coordinates of the bottom right of the canvas. they will be set during _redraw + this.scale = 1; // defining the global scale variable in the constructor // create event listeners used to subscribe on the DataSets of the nodes and edges this.nodesListeners = { - 'add': function (event, params) { - network._addNodes(params.items); - network.start(); + add: function (event, params) { + me._addNodes(params.items); + me.start(); }, - 'update': function (event, params) { - network._updateNodes(params.items, params.data); - network.start(); + update: function (event, params) { + me._updateNodes(params.items, params.data); + me.start(); }, - 'remove': function (event, params) { - network._removeNodes(params.items); - network.start(); + remove: function (event, params) { + me._removeNodes(params.items); + me.start(); } }; this.edgesListeners = { - 'add': function (event, params) { - network._addEdges(params.items); - network.start(); + add: function (event, params) { + me._addEdges(params.items); + me.start(); }, - 'update': function (event, params) { - network._updateEdges(params.items); - network.start(); + update: function (event, params) { + me._updateEdges(params.items); + me.start(); }, - 'remove': function (event, params) { - network._removeEdges(params.items); - network.start(); + remove: function (event, params) { + me._removeEdges(params.items); + me.start(); } }; + // properties for the animation this.moving = true; - this.timer = undefined; // Scheduling function. Is definded in this.start(); + this.renderTimer = undefined; // Scheduling function. Is definded in this.start(); // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled); + this.setData(data, this.constants.hierarchicalLayout.enabled); // hierarchical layout - this.initializing = false; if (this.constants.hierarchicalLayout.enabled == true) { this._setupHierarchicalLayout(); - } - else { + } else { // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. if (this.constants.stabilize == false) { - this.zoomExtent({duration:0}, true, this.constants.clustering.enabled); + this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); } } - // if clustering is disabled, the simulation will have started in the setData function - if (this.constants.clustering.enabled) { - this.startWithClustering(); - } - } - - // Extend Network with an Emitter mixin - Emitter(Network.prototype); - - /** - * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because - * some implementations (safari and IE9) did not support requestAnimationFrame - * @private - */ - Network.prototype._determineBrowserMethod = function() { - var browserType = navigator.userAgent.toLowerCase(); - this.requiresTimeout = false; - if (browserType.indexOf('msie 9.0') != -1) { // IE 9 - this.requiresTimeout = true; + if (this.constants.stabilize == false) { + this.initializing = false; } - else if (browserType.indexOf('safari') != -1) { // safari - if (browserType.indexOf('chrome') <= -1) { - this.requiresTimeout = true; - } - } - } - - /** - * 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 - */ - Network.prototype._getScriptPath = function() { - var scripts = document.getElementsByTagName( 'script' ); - - // find script named vis.js or vis.min.js - for (var i = 0; i < scripts.length; i++) { - var src = scripts[i].src; - var match = src && /\/?vis(.min)?\.js$/.exec(src); - if (match) { - // return path without the script name - return src.substring(0, src.length - match[0].length); + var me = this; + // this event will trigger a rebuilding of the cache of colors, nodes etc. + this.on("_dataChanged", function () { + me._updateNodeIndexList(); + me.physics._updateCalculationNodes(); + me._markAllEdgesAsDirty(); + if (me.initializing !== true) { + me.moving = true; + me.start(); } - } + }); - return null; - }; + this.on("_newEdgesCreated", this._createBezierNodes.bind(this)); + //this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this)); + } + // Extend Network with an Emitter mixin + Emitter(Network.prototype); - /** - * Find the center position of the network - * @private - */ - Network.prototype._getRange = function(specificNodes) { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - if (specificNodes.length > 0) { - for (var i = 0; i < specificNodes.length; i++) { - node = this.nodes[specificNodes[i]]; - if (minX > (node.boundingBox.left)) { - minX = node.boundingBox.left; - } - if (maxX < (node.boundingBox.right)) { - maxX = node.boundingBox.right; - } - if (minY > (node.boundingBox.bottom)) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < (node.boundingBox.top)) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive - } - } - else { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (minX > (node.boundingBox.left)) { - minX = node.boundingBox.left; - } - if (maxX < (node.boundingBox.right)) { - maxX = node.boundingBox.right; - } - if (minY > (node.boundingBox.bottom)) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < (node.boundingBox.top)) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive - } - } - } - if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { - minY = 0, maxY = 0, minX = 0, maxX = 0; - } - return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + Network.prototype._createNode = function (properties) { + return new Node(properties, this.images, this.groups, this.constants); }; - - /** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} - * @private - */ - Network.prototype._findCenter = function(range) { - return {x: (0.5 * (range.maxX + range.minX)), - y: (0.5 * (range.maxY + range.minY))}; + Network.prototype._createEdge = function (properties) { + return new Edge(properties, this.body, this.constants); }; - /** - * This function zooms out to fit all data on screen based on amount of nodes - * - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - * @param {Boolean} [disableStart] | If true, start is not called. - */ - Network.prototype.zoomExtent = function(options, initialZoom, disableStart) { - this._redraw(true); - - if (initialZoom === undefined) {initialZoom = false;} - if (disableStart === undefined) {disableStart = false;} - if (options === undefined) {options = {nodes:[]};} - if (options.nodes === undefined) { - options.nodes = []; - } - - var range; - var zoomLevel; - - if (initialZoom == true) { - // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. - var positionDefined = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.predefinedPosition == true) { - positionDefined += 1; - } - } - } - if (positionDefined > 0.5 * this.nodeIndices.length) { - this.zoomExtent(options,false,disableStart); - return; - } - - range = this._getRange(options.nodes); - - var numberOfNodes = this.nodeIndices.length; - if (this.constants.smoothCurves == true) { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - } - else { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - } - - // correct for larger canvasses. - var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600); - zoomLevel *= factor; - } - else { - range = this._getRange(options.nodes); - var xDistance = Math.abs(range.maxX - range.minX) * 1.1; - var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - - var xZoomLevel = this.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.frame.canvas.clientHeight / yDistance; - zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; - } - - if (zoomLevel > 1.0) { - zoomLevel = 1.0; - } - - - var center = this._findCenter(range); - if (disableStart == false) { - var options = {position: center, scale: zoomLevel, animation: options}; - this.moveTo(options); - this.moving = true; - this.start(); - } - else { - center.x *= zoomLevel; - center.y *= zoomLevel; - center.x -= 0.5 * this.frame.canvas.clientWidth; - center.y -= 0.5 * this.frame.canvas.clientHeight; - this._setScale(zoomLevel); - this._setTranslation(-center.x,-center.y); - } - }; /** - * Update the this.nodeIndices with the most recent node index list + * Update the this.body.nodeIndices with the most recent node index list * @private */ - Network.prototype._updateNodeIndexList = function() { - this._clearNodeIndexList(); - for (var idx in this.nodes) { - if (this.nodes.hasOwnProperty(idx)) { - this.nodeIndices.push(idx); - } - } + Network.prototype._updateNodeIndexList = function () { + this.body.supportNodeIndices = Object.keys(this.body.supportNodes); + this.body.nodeIndices = Object.keys(this.body.nodes); }; @@ -23540,7 +23127,7 @@ return /******/ (function(modules) { // webpackBootstrap * {Options} [options] Object with options * @param {Boolean} [disableStart] | optional: disable the calling of the start function. */ - Network.prototype.setData = function(data, disableStart) { + Network.prototype.setData = function (data, disableStart) { if (disableStart === undefined) { disableStart = false; } @@ -23552,8 +23139,7 @@ return /******/ (function(modules) { // webpackBootstrap this.initializing = true; 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.'); + throw new SyntaxError("Data must contain either parameter \"dot\" or " + " parameter pair \"nodes\" and \"edges\", but not both."); } // clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button. @@ -23566,39 +23152,34 @@ return /******/ (function(modules) { // webpackBootstrap // set all data if (data && data.dot) { // parse DOT file - if(data && data.dot) { + if (data && data.dot) { var dotData = dotparser.DOTToGraph(data.dot); this.setData(dotData); return; } - } - else if (data && data.gephi) { + } else if (data && data.gephi) { // parse DOT file - if(data && data.gephi) { + if (data && data.gephi) { var gephiData = gephiParser.parseGephi(data.gephi); this.setData(gephiData); return; } - } - else { + } else { this._setNodes(data && data.nodes); this._setEdges(data && data.edges); } - this._putDataInSector(); + if (disableStart == false) { if (this.constants.hierarchicalLayout.enabled == true) { this._resetLevels(); this._setupHierarchicalLayout(); + } else { + // find a stable position or start animating to a stable position + this.physics.startSimulation(); } - else { - // find a stable position or start animating to a stable position - if (this.constants.stabilize == true) { - this._stabilize(); - } - } - this.start(); + } else { + this.initializing = false; } - this.initializing = false; }; /** @@ -23608,43 +23189,40 @@ return /******/ (function(modules) { // webpackBootstrap Network.prototype.setOptions = function (options) { if (options) { var prop; - var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation', - 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse' - ]; + var fields = ["nodes", "edges", "smoothCurves", "hierarchicalLayout", "navigation", "keyboard", "dataManipulation", "onAdd", "onEdit", "onEditEdge", "onConnect", "onDelete", "clickToUse"]; // extend all but the values in fields - util.selectiveNotDeepExtend(fields,this.constants, options); - util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes); - util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); + util.selectiveNotDeepExtend(fields, this.constants, options); + util.selectiveNotDeepExtend(["color"], this.constants.nodes, options.nodes); + util.selectiveNotDeepExtend(["color", "length"], this.constants.edges, options.edges); this.groups.useDefaultGroups = this.constants.useDefaultGroups; - if (options.physics) { - util.mergeOptions(this.constants.physics, options.physics,'barnesHut'); - util.mergeOptions(this.constants.physics, options.physics,'repulsion'); - - if (options.physics.hierarchicalRepulsion) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - for (prop in options.physics.hierarchicalRepulsion) { - if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) { - this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop]; - } - } - } - } - if (options.onAdd) {this.triggerFunctions.add = options.onAdd;} - if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;} - if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;} - if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;} - if (options.onDelete) {this.triggerFunctions.del = options.onDelete;} + this.physics.setOptions(options.physics); + this.canvas.setOptions(this.constants); + + + if (options.onAdd) { + this.triggerFunctions.add = options.onAdd; + } + if (options.onEdit) { + this.triggerFunctions.edit = options.onEdit; + } + if (options.onEditEdge) { + this.triggerFunctions.editEdge = options.onEditEdge; + } + if (options.onConnect) { + this.triggerFunctions.connect = options.onConnect; + } + if (options.onDelete) { + this.triggerFunctions.del = options.onDelete; + } - util.mergeOptions(this.constants, options,'smoothCurves'); - util.mergeOptions(this.constants, options,'hierarchicalLayout'); - util.mergeOptions(this.constants, options,'clustering'); - util.mergeOptions(this.constants, options,'navigation'); - util.mergeOptions(this.constants, options,'keyboard'); - util.mergeOptions(this.constants, options,'dataManipulation'); + util.mergeOptions(this.constants, options, "smoothCurves"); + util.mergeOptions(this.constants, options, "hierarchicalLayout"); + util.mergeOptions(this.constants, options, "clustering"); + util.mergeOptions(this.constants, options, "navigation"); + util.mergeOptions(this.constants, options, "keyboard"); + util.mergeOptions(this.constants, options, "dataManipulation"); if (options.dataManipulation) { @@ -23660,19 +23238,27 @@ return /******/ (function(modules) { // webpackBootstrap this.constants.edges.color.color = options.edges.color; this.constants.edges.color.highlight = options.edges.color; this.constants.edges.color.hover = options.edges.color; - } - else { - if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;} - if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;} - if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;} + } else { + if (options.edges.color.color !== undefined) { + this.constants.edges.color.color = options.edges.color.color; + } + if (options.edges.color.highlight !== undefined) { + this.constants.edges.color.highlight = options.edges.color.highlight; + } + if (options.edges.color.hover !== undefined) { + this.constants.edges.color.hover = options.edges.color.hover; + } } this.constants.edges.inheritColor = false; } if (!options.edges.fontColor) { if (options.edges.color !== undefined) { - if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;} - else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;} + if (util.isString(options.edges.color)) { + this.constants.edges.fontColor = options.edges.color; + } else if (options.edges.color.color !== undefined) { + this.constants.edges.fontColor = options.edges.color.color; + } } } } @@ -23708,14 +23294,13 @@ return /******/ (function(modules) { // webpackBootstrap } } - if ('clickToUse' in options) { + if ("clickToUse" in options) { if (options.clickToUse) { if (!this.activator) { this.activator = new Activator(this.frame); - this.activator.on('change', this._createKeyBinds.bind(this)); + this.activator.on("change", this._createKeyBinds.bind(this)); } - } - else { + } else { if (this.activator) { this.activator.destroy(); delete this.activator; @@ -23724,178 +23309,92 @@ return /******/ (function(modules) { // webpackBootstrap } if (options.labels) { - throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.'); + throw new Error("Option \"labels\" is deprecated. Use options \"locale\" and \"locales\" instead."); } - // (Re)loading the mixins that can be enabled or disabled in the options. // load the force calculation functions, grouped under the physics system. - this._loadPhysicsSystem(); // load the navigation system. - this._loadNavigationControls(); - // load the data manipulation system - this._loadManipulationSystem(); - // configure the smooth curves - this._configureSmoothCurves(); + //this._loadNavigationControls(); + //// load the data manipulation system + //this._loadManipulationSystem(); + //// configure the smooth curves + //this._configureSmoothCurves(); // bind hammer - this._bindHammer(); + this.canvas._bindHammer(); // bind keys. If disabled, this will not do anything; - this._createKeyBinds(); + //this._createKeyBinds(); this._markAllEdgesAsDirty(); - this.setSize(this.constants.width, this.constants.height); - this.moving = true; + this.canvas.setSize(this.constants.width, this.constants.height); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } - this.start(); - } - }; - - - - /** - * Create the main frame for the Network. - * This function is executed once when a Network object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - * @private - */ - Network.prototype._create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } - - this.frame = document.createElement('div'); - this.frame.className = 'vis network-frame'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; - this.frame.tabIndex = 900; - - - ////////////////////////////////////////////////////////////////// - this.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); - - if (!this.frame.canvas.getContext) { - var noCanvas = document.createElement( 'DIV' ); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold' ; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); - } - else { - var ctx = this.frame.canvas.getContext("2d"); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || - ctx.mozBackingStorePixelRatio || - ctx.msBackingStorePixelRatio || - ctx.oBackingStorePixelRatio || - ctx.backingStorePixelRatio || 1); - - //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + if (this.initializing !== true) { + this.moving = true; + this.start(); + } } - - this._bindHammer(); }; - /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. - * @private - */ - Network.prototype._bindHammer = function() { - var me = this; - if (this.hammer !== undefined) { - this.hammer.dispose(); - } - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on('tap', me._onTap.bind(me) ); - this.hammer.on('doubletap', me._onDoubleTap.bind(me) ); - this.hammer.on('hold', me._onHold.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) ); - - if (this.constants.zoomable == true) { - this.hammer.on('mousewheel', me._onMouseWheel.bind(me)); - this.hammer.on('DOMMouseScroll', me._onMouseWheel.bind(me)); // for FF - this.hammer.on('pinch', me._onPinch.bind(me) ); - } - - this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); - - this.hammerFrame = Hammer(this.frame, { - prevent_default: true - }); - this.hammerFrame.on('release', me._onRelease.bind(me) ); - - // 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 */ - Network.prototype._createKeyBinds = function() { - var me = this; - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } - - if (this.constants.keyboard.bindToWindow == true) { - this.keycharm = keycharm({container: window, preventDefault: false}); - } - else { - this.keycharm = keycharm({container: this.frame, preventDefault: false}); - } - - this.keycharm.reset(); - - if (this.constants.keyboard.enabled && this.isActive()) { - this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); - this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); - this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); - this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); - this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); - this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); - this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); - this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); - this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); - this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); - this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); - this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); - this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); - } + Network.prototype._createKeyBinds = function () { + return; - if (this.constants.dataManipulation.enabled == true) { - this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); - this.keycharm.bind("delete",this._deleteSelected.bind(me)); - } + //var me = this; + //if (this.keycharm !== undefined) { + // this.keycharm.destroy(); + //} + // + //if (this.constants.keyboard.bindToWindow == true) { + // this.keycharm = keycharm({container: window, preventDefault: false}); + //} + //else { + // this.keycharm = keycharm({container: this.frame, preventDefault: false}); + //} + // + //this.keycharm.reset(); + // + //if (this.constants.keyboard.enabled && this.isActive()) { + // this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); + // this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); + // this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); + // this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); + // this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); + // this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); + // this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); + // this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); + // this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); + // this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); + //} + // + //if (this.constants.dataManipulation.enabled == true) { + // this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); + // this.keycharm.bind("delete",this._deleteSelected.bind(me)); + //} }; /** @@ -23904,10 +23403,10 @@ return /******/ (function(modules) { // webpackBootstrap * network.destroy(); * network = null; */ - Network.prototype.destroy = function() { + Network.prototype.destroy = function () { this.start = function () {}; this.redraw = function () {}; - this.timer = false; + this.renderTimer = false; // cleanup physicsConfiguration if it exists this._cleanupPhysicsConfiguration(); @@ -23922,4639 +23421,3227 @@ return /******/ (function(modules) { // webpackBootstrap this.off(); this._recursiveDOMDelete(this.containerElement); - } + }; - Network.prototype._recursiveDOMDelete = function(DOMobject) { + Network.prototype._recursiveDOMDelete = function (DOMobject) { while (DOMobject.hasChildNodes() == true) { this._recursiveDOMDelete(DOMobject.firstChild); DOMobject.removeChild(DOMobject.firstChild); } - } + }; /** - * Get the pointer location from a touch location - * @param {{pageX: Number, pageY: Number}} touch - * @return {{x: Number, y: Number}} pointer + * Check if there is an element on the given position in the network + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer * @private */ - Network.prototype._getPointer = function (touch) { - return { - x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas), - y: touch.pageY - util.getAbsoluteTop(this.frame.canvas) + Network.prototype._checkShowPopup = function (pointer) { + var obj = { + left: this._XconvertDOMtoCanvas(pointer.x), + top: this._YconvertDOMtoCanvas(pointer.y), + right: this._XconvertDOMtoCanvas(pointer.x), + bottom: this._YconvertDOMtoCanvas(pointer.y) }; - }; - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - Network.prototype._onTouch = function (event) { - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this._getPointer(event.gesture.center); - this.drag.pinched = false; - this.pinch.scale = this._getScale(); + var id; + var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = "node"; - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + if (this.popupObj == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.body.nodes; + var overlappingNodes = []; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.isOverlappingWith(obj)) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(id); + } + } + } + } - this._handleTouch(this.drag.pointer); + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + this.popupObj = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; + } } - }; - - /** - * handle drag start event - * @private - */ - Network.prototype._onDragStart = function (event) { - this._handleDragStart(event); - }; + if (this.popupObj === undefined && nodeUnderCursor == false) { + // search the edges for overlap + var edges = this.body.edges; + var overlappingEdges = []; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected === true && edge.getTitle() !== undefined && edge.isOverlappingWith(obj)) { + overlappingEdges.push(id); + } + } + } - /** - * This function is called by _onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. - * - * @private - */ - Network.prototype._handleDragStart = function(event) { - // in case the touch event was triggered on an external div, do the initial touch now. - if (this.drag.pointer === undefined) { - this._onTouch(event); + if (overlappingEdges.length > 0) { + this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = "edge"; + } } - var node = this._getNodeAt(this.drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location - - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = this._getTranslation(); - this.drag.nodeId = null; - this.draggingNodes = false; - - if (node != null && this.constants.dragNodes == true) { - this.draggingNodes = true; - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectObject(node,false); - } - - this.emit("dragStart",{nodeIds:this.getSelection().nodes}); - - // create an array with the selected nodes and their original location and status - for (var objectId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(objectId)) { - var object = this.selectionObj.nodes[objectId]; - var s = { - id: object.id, - node: object, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed - }; + if (this.popupObj) { + // show popup message window + if (this.popupObj.id != previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new Popup(this.frame, this.constants.tooltip); + } - object.xFixed = true; - object.yFixed = true; + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - this.drag.selection.push(s); - } + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); + } + } else { + if (this.popup) { + this.popup.hide(); } } }; /** - * handle drag event + * Check if the popup must be hidden, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer * @private */ - Network.prototype._onDrag = function (event) { - this._handleOnDrag(event) + Network.prototype._checkHidePopup = function (pointer) { + var pointerObj = { + left: this._XconvertDOMtoCanvas(pointer.x), + top: this._YconvertDOMtoCanvas(pointer.y), + right: this._XconvertDOMtoCanvas(pointer.x), + bottom: this._YconvertDOMtoCanvas(pointer.y) + }; + + var stillOnObj = false; + if (this.popup.popupTargetType == "node") { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + if (stillOnObj === true) { + var overNode = this._getNodeAt(pointer); + stillOnObj = overNode.id == this.popup.popupTargetId; + } + } else { + if (this._getNodeAt(pointer) === null) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } + } + + + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); + } }; /** - * This function is called by _onDrag. - * It is separated out because we can then overload it for the datamanipulation system. - * + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. * @private */ - Network.prototype._handleOnDrag = function(event) { - if (this.drag.pinched) { - return; - } - - // remove the focus on node if it is focussed on by the focusOnNode - this.releaseNode(); + Network.prototype._setNodes = function (nodes) { + var oldNodesData = this.body.data.nodes; - var pointer = this._getPointer(event.gesture.center); - var me = this; - var drag = this.drag; - var selection = drag.selection; - if (selection && selection.length && this.constants.dragNodes == true) { - // calculate delta's and new location - var deltaX = pointer.x - drag.pointer.x; - var deltaY = pointer.y - drag.pointer.y; - - // update position of all selected nodes - selection.forEach(function (s) { - var node = s.node; - - if (!s.xFixed) { - node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); - } + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.body.data.nodes = nodes; + } else if (Array.isArray(nodes)) { + this.body.data.nodes = new DataSet(); + this.body.data.nodes.add(nodes); + } else if (!nodes) { + this.body.data.nodes = new DataSet(); + } else { + throw new TypeError("Array or DataSet expected"); + } - if (!s.yFixed) { - node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); - } + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); }); + } + // remove drawn nodes + this.body.nodes = {}; - // start _animationStep if not yet running - if (!this.moving) { - this.moving = true; - this.start(); - } - } - else { - // move the network - if (this.constants.dragNetwork == true) { - // if the drag was not started properly because the click started outside the network div, start it now. - if (this.drag.pointer === undefined) { - this._handleDragStart(event); - return; - } - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + if (this.body.data.nodes) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.body.data.nodes.on(event, callback); + }); - this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY - ); - this._redraw(); - } + // draw all new nodes + var ids = this.body.data.nodes.getIds(); + this._addNodes(ids); } + this._updateSelection(); }; /** - * handle drag start event + * Add nodes + * @param {Number[] | String[]} ids * @private */ - Network.prototype._onDragEnd = function (event) { - this._handleDragEnd(event); - }; - - - Network.prototype._handleDragEnd = function(event) { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - this.moving = true; - this.start(); - } - else { - this._redraw(); - } - if (this.draggingNodes == false) { - this.emit("dragEnd",{nodeIds:[]}); - } - else { - this.emit("dragEnd",{nodeIds:this.getSelection().nodes}); + Network.prototype._addNodes = function (ids) { + var id; + for (var i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + var data = this.body.data.nodes.get(id); + var node = new Node(data, this.images, this.groups, this.constants); + this.body.nodes[id] = node; // note: this may replace an existing node + if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { + var radius = 10 * 0.1 * ids.length + 10; + var angle = 2 * Math.PI * Math.random(); + if (node.xFixed == false) { + node.x = radius * Math.cos(angle); + } + if (node.yFixed == false) { + node.y = radius * Math.sin(angle); + } + } + this.moving = true; } - } - /** - * handle tap/click event: select/unselect a node - * @private - */ - Network.prototype._onTap = function (event) { - var pointer = this._getPointer(event.gesture.center); - this.pointerPosition = pointer; - this._handleTap(pointer); - + this._updateNodeIndexList(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.physics._updateCalculationNodes(); + this._reconnectEdges(); + this._updateValueRange(this.body.nodes); }; - /** - * handle doubletap event + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids * @private */ - Network.prototype._onDoubleTap = function (event) { - var pointer = this._getPointer(event.gesture.center); - this._handleDoubleTap(pointer); + Network.prototype._updateNodes = function (ids, changedData) { + var nodes = this.body.nodes; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node) { + // update node + node.setProperties(data, this.constants); + } else { + // create node + node = new Node(properties, this.images, this.groups, this.constants); + nodes[id] = node; + } + } + this.moving = true; + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this._updateNodeIndexList(); + this._updateValueRange(nodes); + this._markAllEdgesAsDirty(); }; - /** - * handle long tap event: multi select nodes - * @private - */ - Network.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.center); - this.pointerPosition = pointer; - this._handleOnHold(pointer); + Network.prototype._markAllEdgesAsDirty = function () { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].colorDirty = true; + } }; /** - * handle the release of the screen - * + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids * @private */ - Network.prototype._onRelease = function (event) { - var pointer = this._getPointer(event.gesture.center); - this._handleOnRelease(pointer); - }; + Network.prototype._removeNodes = function (ids) { + var nodes = this.body.nodes; - /** - * Handle pinch event - * @param event - * @private - */ - Network.prototype._onPinch = function (event) { - var pointer = this._getPointer(event.gesture.center); + // remove from selection + for (var i = 0, len = ids.length; i < len; i++) { + if (this.selectionObj.nodes[ids[i]] !== undefined) { + this.body.nodes[ids[i]].unselect(); + this._removeFromSelection(this.body.nodes[ids[i]]); + } + } - this.drag.pinched = true; - if (!('scale' in this.pinch)) { - this.pinch.scale = 1; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + delete nodes[id]; } - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this._zoom(scale, pointer) + + + this._updateNodeIndexList(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.physics._updateCalculationNodes(); + this._reconnectEdges(); + this._updateSelection(); + this._updateValueRange(nodes); }; /** - * Zoom the network in or out - * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer Position on screen - * @return {Number} appliedScale scale is limited within the boundaries + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private * @private */ - Network.prototype._zoom = function(scale, pointer) { - if (this.constants.zoomable == true) { - var scaleOld = this._getScale(); - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } + Network.prototype._setEdges = function (edges) { + var oldEdgesData = this.body.data.edges; - var preScaleDragPointer = null; - if (this.drag !== undefined) { - if (this.drag.dragging == true) { - preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer); - } - } - // + this.frame.canvas.clientHeight / 2 - var translation = this._getTranslation(); + if (edges instanceof DataSet || edges instanceof DataView) { + this.body.data.edges = edges; + } else if (Array.isArray(edges)) { + this.body.data.edges = new DataSet(); + this.body.data.edges.add(edges); + } else if (!edges) { + this.body.data.edges = new DataSet(); + } else { + throw new TypeError("Array or DataSet expected"); + } - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); + } - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; + // remove drawn edges + this.body.edges = {}; - this._setScale(scale); - this._setTranslation(tx, ty); - this.updateClustersDefault(); + if (this.body.data.edges) { + // subscribe to new dataset + var me = this; + util.forEach(this.edgesListeners, function (callback, event) { + me.body.data.edges.on(event, callback); + }); - if (preScaleDragPointer != null) { - var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this._addEdges(ids); + } - this._redraw(); + this._reconnectEdges(); + }; - if (scaleOld < scale) { - this.emit("zoom", {direction:"+"}); - } - else { - this.emit("zoom", {direction:"-"}); + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._addEdges = function (ids) { + var edges = this.body.edges, + edgesData = this.body.data.edges; + + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); } - return scale; + var data = edgesData.get(id, { showInternalIds: true }); + edges[id] = new Edge(data, this.body, this.constants); + } + this.moving = true; + this._updateValueRange(edges); + this._createBezierNodes(); + this.physics._updateCalculationNodes(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); } }; - /** - * Event 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 + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids * @private */ - Network.prototype._onMouseWheel = function(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta/120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail/3; - } - - // 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) { + Network.prototype._updateEdges = function (ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; - // calculate the new scale - var scale = this._getScale(); - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); + var data = edgesData.get(id); + var edge = edges[id]; + if (edge) { + // update edge + edge.disconnect(); + edge.setProperties(data); + edge.connect(); + } else { + // create edge + edge = new Edge(data, this.body, this.constants); + this.body.edges[id] = edge; } - scale *= (1 + zoom); - - // calculate the pointer location - var gesture = hammerUtil.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); - - // apply the new scale - this._zoom(scale, pointer); } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); + this._createBezierNodes(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.moving = true; + this._updateValueRange(edges); }; - /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids * @private */ - Network.prototype._onMouseMoveTitle = function (event) { - var gesture = hammerUtil.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); - var popupVisible = false; + Network.prototype._removeEdges = function (ids) { + var edges = this.body.edges; - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); + // remove from selection + for (var i = 0, len = ids.length; i < len; i++) { + if (this.selectionObj.edges[ids[i]] !== undefined) { + edges[ids[i]].unselect(); + this._removeFromSelection(edges[ids[i]]); } + } - // if the popup was not hidden above - if (this.popup.hidden === false) { - popupVisible = true; - this.popup.setPosition(pointer.x + 3,pointer.y - 5) - this.popup.show(); + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge) { + if (edge.via != null) { + delete this.body.supportNodes[edge.via.id]; + } + edge.disconnect(); + delete edges[id]; } } - // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over - if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { - this.frame.focus(); + this.moving = true; + this._updateValueRange(edges); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); } + this.physics._updateCalculationNodes(); + }; - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - var me = this; - var checkShow = function () { - me._checkShowPopup(pointer); - }; - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running calculationTimer - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); + /** + * Reconnect all edges + * @private + */ + Network.prototype._reconnectEdges = function () { + var id, + nodes = this.body.nodes, + edges = this.body.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; } } - /** - * Adding hover highlights - */ - if (this.constants.hover == true) { - // removing all hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); } + } + }; - // adding hover highlights - var obj = this._getNodeAt(pointer); - if (obj == null) { - obj = this._getEdgeAt(pointer); - } - if (obj != null) { - this._hoverObject(obj); + /** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Object} obj An object containing a set of Edges or Nodes + * The objects must have a method getValue() and + * setValueRange(min, max). + * @private + */ + Network.prototype._updateValueRange = function (obj) { + var id; + + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + var valueTotal = 0; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = valueMin === undefined ? value : Math.min(value, valueMin); + valueMax = valueMax === undefined ? value : Math.max(value, valueMax); + valueTotal += value; + } } + } - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - this._blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { + for (id in obj) { + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax, valueTotal); } } - this.redraw(); } }; + /** - * Check if there is an element on the given position in the network - * (a node or edge). If so, and if this element has a title, - * show a popup window with its title. - * - * @param {{x:Number, y:Number}} pointer - * @private + * Set the translation of the network + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + * @private */ - Network.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._XconvertDOMtoCanvas(pointer.x), - top: this._YconvertDOMtoCanvas(pointer.y), - right: this._XconvertDOMtoCanvas(pointer.x), - bottom: this._YconvertDOMtoCanvas(pointer.y) - }; - - var id; - var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = "node"; - - if (this.popupObj == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.nodes; - var overlappingNodes = []; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.isOverlappingWith(obj)) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(id); - } - } - } - } - - if (overlappingNodes.length > 0) { - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - this.popupObj = this.nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = true; - } - } - - if (this.popupObj === undefined && nodeUnderCursor == false) { - // search the edges for overlap - var edges = this.edges; - var overlappingEdges = []; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected && (edge.getTitle() !== undefined) && - edge.isOverlappingWith(obj)) { - overlappingEdges.push(id); - } - } - } - - if (overlappingEdges.length > 0) { - this.popupObj = this.edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = "edge"; - } + Network.prototype._setTranslation = function (offsetX, offsetY) { + if (this.translation === undefined) { + this.translation = { + x: 0, + y: 0 + }; } - if (this.popupObj) { - // show popup message window - if (this.popupObj.id != previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new Popup(this.frame, this.constants.tooltip); - } - - this.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; - - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.setText(this.popupObj.getTitle()); - this.popup.show(); - } + if (offsetX !== undefined) { + this.translation.x = offsetX; } - else { - if (this.popup) { - this.popup.hide(); - } + if (offsetY !== undefined) { + this.translation.y = offsetY; } - }; + this.emit("viewChanged"); + }; /** - * Check if the popup must be hidden, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer + * Get the translation of the network + * @return {Object} translation An object with parameters x and y, both a number * @private */ - Network.prototype._checkHidePopup = function (pointer) { - var pointerObj = { - left: this._XconvertDOMtoCanvas(pointer.x), - top: this._YconvertDOMtoCanvas(pointer.y), - right: this._XconvertDOMtoCanvas(pointer.x), - bottom: this._YconvertDOMtoCanvas(pointer.y) + Network.prototype._getTranslation = function () { + return { + x: this.translation.x, + y: this.translation.y }; - - var stillOnObj = false; - if (this.popup.popupTargetType == 'node') { - stillOnObj = this.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); - if (stillOnObj === true) { - var overNode = this._getNodeAt(pointer); - stillOnObj = overNode.id == this.popup.popupTargetId; - } - } - else { - if (this._getNodeAt(pointer) === null) { - stillOnObj = this.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); - } - } - - - if (stillOnObj === false) { - this.popupObj = undefined; - this.popup.hide(); - } }; - /** - * Set a new size for the network - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') + * Scale the network + * @param {Number} scale Scaling factor 1.0 is unscaled + * @private */ - Network.prototype.setSize = function(width, height) { - var emitEvent = false; - var oldWidth = this.frame.canvas.width; - var oldHeight = this.frame.canvas.height; - if (width != this.constants.width || height != this.constants.height || this.frame.style.width != width || this.frame.style.height != height) { - this.frame.style.width = width; - this.frame.style.height = height; - - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; - - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - - this.constants.width = width; - this.constants.height = height; - - emitEvent = true; - } - else { - // this would adapt the width of the canvas to the width from 100% if and only if - // there is a change. - - if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - emitEvent = true; - } - if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - emitEvent = true; - } - } - - if (emitEvent == true) { - this.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio}); - } + Network.prototype._setScale = function (scale) { + this.scale = scale; }; /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * Get the current scale of the network + * @return {Number} scale Scaling factor 1.0 is unscaled * @private */ - Network.prototype._setNodes = function(nodes) { - var oldNodesData = this.nodesData; + Network.prototype._getScale = function () { + return this.scale; + }; - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.nodesData = nodes; - } - else if (Array.isArray(nodes)) { - 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.off(event, callback); - }); + /** + * Move the network according to the keyboard presses. + * + * @private + */ + Network.prototype._handleNavigation = function () { + if (this.xIncrement != 0 || this.yIncrement != 0) { + var translation = this._getTranslation(); + this._setTranslation(translation.x + this.xIncrement, translation.y + this.yIncrement); } - - // remove drawn nodes - this.nodes = {}; - - if (this.nodesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.nodesData.on(event, callback); - }); - - // draw all new nodes - var ids = this.nodesData.getIds(); - this._addNodes(ids); + if (this.zoomIncrement != 0) { + var center = { + x: this.frame.canvas.clientWidth / 2, + y: this.frame.canvas.clientHeight / 2 + }; + this._zoom(this.scale * (1 + this.zoomIncrement), center); } - this._updateSelection(); }; + /** - * Add nodes - * @param {Number[] | String[]} ids - * @private + * Freeze the _animationStep */ - Network.prototype._addNodes = function(ids) { - var id; - for (var i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - var data = this.nodesData.get(id); - var node = new Node(data, this.images, this.groups, this.constants); - this.nodes[id] = node; // note: this may replace an existing node - if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { - var radius = 10 * 0.1*ids.length + 10; - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} - } + Network.prototype.freezeSimulation = function (freeze) { + if (freeze == true) { + this.freezeSimulationEnabled = true; + this.moving = false; + } else { + this.freezeSimulationEnabled = false; this.moving = true; + this.start(); } - - this._updateNodeIndexList(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this._updateCalculationNodes(); - this._reconnectEdges(); - this._updateValueRange(this.nodes); - this.updateLabels(); }; + /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids + * This function cleans the support nodes if they are not needed and adds them when they are. + * + * @param {boolean} [disableStart] * @private */ - Network.prototype._updateNodes = function(ids,changedData) { - var nodes = this.nodes; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node) { - // update node - node.setProperties(data, this.constants); + Network.prototype._configureSmoothCurves = function () { + var disableStart = arguments[0] === undefined ? true : arguments[0]; + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this._createBezierNodes(); + // cleanup unused support nodes + for (var i = 0; i < this.body.supportNodeIndices.length; i++) { + var nodeId = this.body.supportNodeIndices[i]; + // delete support nodes for edges that have been deleted + if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) { + delete this.body.supportNodes[nodeId]; + } } - else { - // create node - node = new Node(properties, this.images, this.groups, this.constants); - nodes[id] = node; + } else { + // delete the support nodes + this.body.supportNodes = {}; + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId].via = null; + } } } - this.moving = true; - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this._updateNodeIndexList(); - this._updateValueRange(nodes); - this._markAllEdgesAsDirty(); - }; - Network.prototype._markAllEdgesAsDirty = function() { - for (var edgeId in this.edges) { - this.edges[edgeId].colorDirty = true; + this._updateNodeIndexList(); + this.physics._updateCalculationNodes(); + if (!disableStart) { + this.moving = true; + this.start(); } - } + }; + /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids + * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but + * are used for the force calculation. + * * @private */ - Network.prototype._removeNodes = function(ids) { - var nodes = this.nodes; - - // remove from selection - for (var i = 0, len = ids.length; i < len; i++) { - if (this.selectionObj.nodes[ids[i]] !== undefined) { - this.nodes[ids[i]].unselect(); - this._removeFromSelection(this.nodes[ids[i]]); + Network.prototype._createBezierNodes = function () { + var specificEdges = arguments[0] === undefined ? this.body.edges : arguments[0]; + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + for (var edgeId in specificEdges) { + if (specificEdges.hasOwnProperty(edgeId)) { + var edge = specificEdges[edgeId]; + if (edge.via == null) { + var nodeId = "edgeId:".concat(edge.id); + var node = new Node({ id: nodeId, + mass: 1, + shape: "circle", + image: "", + internalMultiplier: 1 + }, {}, {}, this.constants); + this.body.supportNodes[nodeId] = node; + edge.via = node; + edge.via.parentEdgeId = edge.id; + edge.positionBezierNode(); + } + } } + this._updateNodeIndexList(); } - - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - delete nodes[id]; - } - - - - this._updateNodeIndexList(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this._updateCalculationNodes(); - 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 + * load the functions that load the mixins into the prototype. + * * @private */ - Network.prototype._setEdges = function(edges) { - var oldEdgesData = this.edgesData; - - if (edges instanceof DataSet || edges instanceof DataView) { - this.edgesData = edges; - } - else if (Array.isArray(edges)) { - 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.off(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.on(event, callback); - }); - - // draw all new nodes - var ids = this.edgesData.getIds(); - this._addEdges(ids); + Network.prototype._initializeMixinLoaders = function () { + for (var mixin in MixinLoader) { + if (MixinLoader.hasOwnProperty(mixin)) { + Network.prototype[mixin] = MixinLoader[mixin]; + } } - - this._reconnectEdges(); }; /** - * Add edges - * @param {Number[] | String[]} ids - * @private + * Load the XY positions of the nodes into the dataset. */ - Network.prototype._addEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - - 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); - this._createBezierNodes(); - this._updateCalculationNodes(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } + Network.prototype.storePosition = function () { + console.log("storePosition is depricated: use .storePositions() from now on."); + this.storePositions(); }; /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private + * Load the XY positions of the nodes into the dataset. */ - Network.prototype._updateEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - - 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; + Network.prototype.storePositions = function () { + var dataArray = []; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + var allowedToMoveX = !this.body.nodes.xFixed; + var allowedToMoveY = !this.body.nodes.yFixed; + if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { + dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y), allowedToMoveX: allowedToMoveX, allowedToMoveY: allowedToMoveY }); + } } } - - this._createBezierNodes(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this.moving = true; - this._updateValueRange(edges); + this.body.data.nodes.update(dataArray); }; /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private + * Return the positions of the nodes. */ - Network.prototype._removeEdges = function (ids) { - var edges = this.edges; - - // remove from selection - for (var i = 0, len = ids.length; i < len; i++) { - if (this.selectionObj.edges[ids[i]] !== undefined) { - edges[ids[i]].unselect(); - this._removeFromSelection(edges[ids[i]]); + Network.prototype.getPositions = function (ids) { + var dataArray = {}; + if (ids !== undefined) { + if (Array.isArray(ids) == true) { + for (var i = 0; i < ids.length; i++) { + if (this.body.nodes[ids[i]] !== undefined) { + var node = this.body.nodes[ids[i]]; + dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + if (this.body.nodes[ids] !== undefined) { + var node = this.body.nodes[ids]; + dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; + } } - } - - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge) { - if (edge.via != null) { - delete this.sectors['support']['nodes'][edge.via.id]; + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; } - edge.disconnect(); - delete edges[id]; } } - - this.moving = true; - this._updateValueRange(edges); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this._updateCalculationNodes(); + return dataArray; }; + /** - * Reconnect all edges - * @private + * Returns true when the Network is active. + * @returns {boolean} */ - Network.prototype._reconnectEdges = function() { - var id, - nodes = this.nodes, - edges = this.edges; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - nodes[id].dynamicEdges = []; - } - } - - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } + Network.prototype.isActive = function () { + return !this.activator || this.activator.active; }; + /** - * 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 + * Sets the scale + * @returns {Number} */ - Network.prototype._updateValueRange = function(obj) { - var id; - - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - var valueTotal = 0; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); - valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); - valueTotal += value; - } - } - } - - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax, valueTotal); - } - } - } + Network.prototype.setScale = function () { + return this._setScale(); }; + /** - * Redraw the network with the current data - * chart will be resized too. + * Returns the scale + * @returns {Number} */ - Network.prototype.redraw = function() { - this.setSize(this.constants.width, this.constants.height); - this._redraw(); + Network.prototype.getScale = function () { + return this._getScale(); }; + /** - * Redraw the network with the current data - * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. - * @private + * Check if a node is a cluster. + * @param nodeId + * @returns {*} */ - Network.prototype._requestRedraw = function(hidden) { - if (this.redrawRequested !== true) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, hidden),0); - } - else { - window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); - } + Network.prototype.isCluster = function (nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster; + } else { + console.log("Node does not exist."); + return false; } }; - Network.prototype._redraw = function(hidden, requested) { - if (hidden === undefined) { - hidden = false; - } - this.redrawRequested = false; - var ctx = this.frame.canvas.getContext('2d'); - - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - - // clear the canvas - var w = this.frame.canvas.clientWidth; - var h = this.frame.canvas.clientHeight; - ctx.clearRect(0, 0, w, h); + /** + * Returns the scale + * @returns {Number} + */ + Network.prototype.getCenterCoordinates = function () { + return this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); + }; - // set scaling and translation - ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); - this.canvasTopLeft = { - "x": this._XconvertDOMtoCanvas(0), - "y": this._YconvertDOMtoCanvas(0) - }; - this.canvasBottomRight = { - "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth), - "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight) - }; + Network.prototype.getBoundingBox = function (nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].boundingBox; + } + }; - if (hidden === false) { - this._doInAllSectors("_drawAllSectorNodes", ctx); - if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { - this._doInAllSectors("_drawEdges", ctx); + Network.prototype.getConnectedNodes = function (nodeId) { + var nodeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + var nodeObj = { nodeId: true }; // used to quickly check if node already exists + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + if (edge.toId == nodeId) { + if (nodeObj[edge.fromId] === undefined) { + nodeList.push(edge.fromId); + nodeObj[edge.fromId] = true; + } + } else if (edge.fromId == nodeId) { + if (nodeObj[edge.toId] === undefined) { + nodeList.push(edge.toId); + nodeObj[edge.toId] = true; + } + } } } + return nodeList; + }; - if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { - this._doInAllSectors("_drawNodes",ctx,false); - } - if (hidden === false) { - if (this.controlNodesActive == true) { - this._doInAllSectors("_drawControlNodes", ctx); + Network.prototype.getEdgesFromNode = function (nodeId) { + var edgesList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + for (var i = 0; i < node.edges.length; i++) { + edgesList.push(node.edges[i].id); } } + return edgesList; + }; - // this._doInSupportSector("_drawNodes",ctx,true); - // this._drawTree(ctx,"#F00F0F"); + Network.prototype.generateColorObject = function (color) { + return util.parseColor(color); + }; - // restore original scaling and translation - ctx.restore(); + module.exports = Network; - if (hidden === true) { - ctx.clearRect(0, 0, w, h); - } - } +/***/ }, +/* 52 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; /** - * Set the translation of the network - * @param {Number} offsetX Horizontal offset - * @param {Number} offsetY Vertical offset - * @private - */ - Network.prototype._setTranslation = function(offsetX, offsetY) { - if (this.translation === undefined) { - this.translation = { - x: 0, - y: 0 - }; - } - - if (offsetX !== undefined) { - this.translation.x = offsetX; - } - if (offsetY !== undefined) { - this.translation.y = offsetY; - } - - this.emit('viewChanged'); - }; - - /** - * Get the translation of the network - * @return {Object} translation An object with parameters x and y, both a number - * @private - */ - Network.prototype._getTranslation = function() { - return { - x: this.translation.x, - y: this.translation.y - }; - }; - - /** - * Scale the network - * @param {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._setScale = function(scale) { - this.scale = scale; - }; - - /** - * Get the current scale of the network - * @return {Number} scale Scaling factor 1.0 is unscaled - * @private + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges */ - Network.prototype._getScale = function() { - return this.scale; - }; + function parseDOT(data) { + dot = data; + return parseGraph(); + } - /** - * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} x - * @returns {number} - * @private - */ - Network.prototype._XconvertDOMtoCanvas = function(x) { - return (x - this.translation.x) / this.scale; + // token types enumeration + var TOKENTYPE = { + NULL: 0, + DELIMITER: 1, + IDENTIFIER: 2, + UNKNOWN: 3 }; - /** - * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the X coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} x - * @returns {number} - * @private - */ - Network.prototype._XconvertCanvasToDOM = function(x) { - return x * this.scale + this.translation.x; - }; + // map with all delimiters + var DELIMITERS = { + "{": true, + "}": true, + "[": true, + "]": true, + ";": true, + "=": true, + ",": true, + + "->": true, + "--": true + }; + + var dot = ""; // current dot file + var index = 0; // current index in dot file + var c = ""; // current token character in expr + var token = ""; // current token + var tokenType = TOKENTYPE.NULL; // type of the token /** - * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} y - * @returns {number} - * @private + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. */ - Network.prototype._YconvertDOMtoCanvas = function(y) { - return (y - this.translation.y) / this.scale; - }; + function first() { + index = 0; + c = dot.charAt(0); + } /** - * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} y - * @returns {number} - * @private + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. */ - Network.prototype._YconvertCanvasToDOM = function(y) { - return y * this.scale + this.translation.y ; - }; - + function next() { + index++; + c = dot.charAt(index); + } /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor + * Preview the next character from the dot file. + * @return {String} cNext */ - Network.prototype.canvasToDOM = function (pos) { - return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)}; - }; + function nextPreview() { + return dot.charAt(index + 1); + } /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric */ - Network.prototype.DOMtoCanvas = function (pos) { - return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)}; - }; + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); + } /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] - * @private + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a */ - Network.prototype._drawNodes = function(ctx,alwaysShow) { - if (alwaysShow === undefined) { - alwaysShow = false; + function merge(a, b) { + if (!a) { + a = {}; } - // 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); - } + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; } } } - - // 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); - } - } - }; + return a; + } /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value */ - Network.prototype._drawEdges = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.setScale(this.scale); - if (edge.connected) { - edges[id].draw(ctx); + function setValue(obj, path, value) { + var keys = path.split("."); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; } + o = o[key]; + } else { + // this is the end point + o[key] = value; } } - }; + } /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node */ - Network.prototype._drawControlNodes = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - edges[id]._drawControlNodes(ctx); - } + function addNode(graph, node) { + var i, len; + var current = null; + + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; } - }; - /** - * Find a stable position for all nodes - * @private - */ - Network.prototype._stabilize = function() { - if (this.constants.freezeForStabilization == true) { - this._freezeDefinedNodes(); + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; + } + } } - // find stable position - var count = 0; - while (this.moving && count < this.constants.stabilizationIterations) { - this._physicsTick(); - count++; + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } } + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - if (this.constants.zoomExtentOnStabilize == true) { - this.zoomExtent({duration:0}, false, true); + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } } - if (this.constants.freezeForStabilization == true) { - this._restoreFrozenNodes(); + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); } - - this.emit("stabilizationIterationsDone"); - }; + } /** - * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization - * because only the supportnodes for the smoothCurves have to settle. - * - * @private + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge */ - Network.prototype._freezeDefinedNodes = function() { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x != null && nodes[id].y != null) { - nodes[id].fixedData.x = nodes[id].xFixed; - nodes[id].fixedData.y = nodes[id].yFixed; - nodes[id].xFixed = true; - nodes[id].yFixed = true; - } - } + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; } - }; + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes + } + } /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge */ - Network.prototype._restoreFrozenNodes = function() { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].fixedData.x != null) { - nodes[id].xFixed = nodes[id].fixedData.x; - nodes[id].yFixed = nodes[id].fixedData.y; - } - } + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; + + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes } - }; + edge.attr = merge(edge.attr || {}, attr); // merge attributes + return edge; + } /** - * 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 + * Get next token in the current dot file. + * The token and token type are available as token and tokenType */ - Network.prototype._isMoving = function(vmin) { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes[id] !== undefined) { - if (nodes[id].isMoving(vmin) == true) { - return true; - } - } - } - return false; - }; + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ""; + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } - /** - * /** - * Perform one discrete step for all nodes - * - * @private - */ - Network.prototype._discreteStepNodes = function() { - var interval = this.physicsDiscreteStepsize; - var nodes = this.nodes; - var nodeId; - var nodesPresent = false; + do { + var isComment = false; - if (this.constants.maxVelocity > 0) { - for (nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity); - nodesPresent = true; + // skip comment + if (c == "#") { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == " " || dot.charAt(i) == "\t") { + i--; } - } - } - else { - for (nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].discreteStep(interval); - nodesPresent = true; + if (dot.charAt(i) == "\n" || dot.charAt(i) == "") { + // the # is at the start of a line, this is indeed a line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; } } - } - - if (nodesPresent == true) { - var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05); - if (vminCorrected > 0.5*this.constants.maxVelocity) { - return true; + if (c == "/" && nextPreview() == "/") { + // skip line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; } - else { - return this._isMoving(vminCorrected); + if (c == "/" && nextPreview() == "*") { + // skip block comment + while (c != "") { + if (c == "*" && nextPreview() == "/") { + // end of block comment found. skip these last two characters + next(); + next(); + break; + } else { + next(); + } + } + isComment = true; } - } - return false; - }; - - Network.prototype._revertPhysicsState = function() { - var nodes = this.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].revertPosition(); + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); } + } while (isComment); + + // check for end of dot file + if (c == "") { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; } - } - Network.prototype._revertPhysicsTick = function() { - this._doInAllActiveSectors("_revertPhysicsState"); - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._doInSupportSector("_revertPhysicsState"); + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; } - } - /** - * A single simulation step (or "tick") in the physics simulation - * - * @private - */ - Network.prototype._physicsTick = function() { - if (!this.freezeSimulationEnabled) { - if (this.moving == true) { - var mainMovingStatus = false; - var supportMovingStatus = false; + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } - this._doInAllActiveSectors("_initializeForceCalculation"); - var mainMoving = this._doInAllActiveSectors("_discreteStepNodes"); - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - supportMovingStatus = this._doInSupportSector("_discreteStepNodes"); - } + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == "-") { + token += c; + next(); - // gather movement data from all sectors, if one moves, we are NOT stabilzied - for (var i = 0; i < mainMoving.length; i++) { - mainMovingStatus = mainMoving[i] || mainMovingStatus; - } + while (isAlphaNumeric(c)) { + token += c; + next(); + } + if (token == "false") { + token = false; // convert to boolean + } else if (token == "true") { + token = true; // convert to boolean + } else if (!isNaN(Number(token))) { + token = Number(token); // convert to number + } + tokenType = TOKENTYPE.IDENTIFIER; + return; + } - // determine if the network has stabilzied - this.moving = mainMovingStatus || supportMovingStatus; - if (this.moving == false) { - this._revertPhysicsTick(); - } - else { - // this is here to ensure that there is no start event when the network is already stable. - if (this.startedStabilization == false) { - this.emit("startStabilization"); - this.startedStabilization = true; - } + // check for a string enclosed by double quotes + if (c == "\"") { + next(); + while (c != "" && (c != "\"" || c == "\"" && nextPreview() == "\"")) { + token += c; + if (c == "\"") { + // skip the escape character + next(); } - - this.stabilizationIterations++; + next(); } + if (c != "\"") { + throw newSyntaxError("End of string \" expected"); + } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; } - }; + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != "") { + token += c; + next(); + } + throw new SyntaxError("Syntax error in part \"" + chop(token, 30) + "\""); + } /** - * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick. - * It reschedules itself at the beginning of the function - * - * @private + * Parse a graph. + * @returns {Object} graph */ - Network.prototype._animationStep = function() { - // reset the timer so a new scheduled animation step can be set - this.timer = undefined; + function parseGraph() { + var graph = {}; - if (this.requiresTimeout == true) { - // this schedules a new animation step - this.start(); - } + first(); + getToken(); - // handle the keyboad movement - this._handleNavigation(); + // optional strict keyword + if (token == "strict") { + graph.strict = true; + getToken(); + } - // check if the physics have settled - if (this.moving == true) { - var startTime = Date.now(); - this._physicsTick(); - var physicsTime = Date.now() - startTime; + // graph or digraph keyword + if (token == "graph" || token == "digraph") { + graph.type = token; + getToken(); + } - // run double speed if it is a little graph - if ((this.renderTimestep - this.renderTime > 2 * physicsTime || this.runDoubleSpeed == true) && this.moving == true) { - this._physicsTick(); + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - if (this.renderTime != 0) { - this.runDoubleSpeed = true - } - } + // open angle bracket + if (token != "{") { + throw newSyntaxError("Angle bracket { expected"); } + getToken(); - var renderStartTime = Date.now(); - this._redraw(); - this.renderTime = Date.now() - renderStartTime; + // statements + parseStatements(graph); - if (this.requiresTimeout == false) { - // this schedules a new animation step - this.start(); + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); } - }; + getToken(); + + // end of file + if (token !== "") { + throw newSyntaxError("End of file expected"); + } + getToken(); + + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; - if (typeof window !== 'undefined') { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + return graph; } /** - * Schedule a animation step with the refreshrate interval. + * Parse a list with statements. + * @param {Object} graph */ - Network.prototype.start = function() { - if (this.freezeSimulationEnabled == true) { - this.moving = false; - } - if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0 || this.animating == true) { - if (!this.timer) { - if (this.requiresTimeout == true) { - this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function - } - else { - this.timer = window.requestAnimationFrame(this._animationStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function - } - } - } - else { - this._requestRedraw(); - // this check is to ensure that the network does not emit these events if it was already stabilized and setOptions is called (setting moving to true and calling start()) - if (this.stabilizationIterations > 1) { - // trigger the "stabilized" event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - var me = this; - var params = { - iterations: me.stabilizationIterations - }; - this.stabilizationIterations = 0; - this.startedStabilization = false; - setTimeout(function () { - me.emit("stabilized", params); - }, 0); - } - else { - this.stabilizationIterations = 0; + function parseStatements(graph) { + while (token !== "" && token != "}") { + parseStatement(graph); + if (token == ";") { + getToken(); } } - }; - + } /** - * Move the network according to the keyboard presses. - * - * @private + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph */ - Network.prototype._handleNavigation = function() { - if (this.xIncrement != 0 || this.yIncrement != 0) { - var translation = this._getTranslation(); - this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement); - } - if (this.zoomIncrement != 0) { - var center = { - x: this.frame.canvas.clientWidth / 2, - y: this.frame.canvas.clientHeight / 2 - }; - this._zoom(this.scale*(1 + this.zoomIncrement), center); - } - }; + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); + return; + } - /** - * Freeze the _animationStep - */ - Network.prototype.freezeSimulation = function(freeze) { - if (freeze == true) { - this.freezeSimulationEnabled = true; - this.moving = false; + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; } - else { - this.freezeSimulationEnabled = false; - this.moving = true; - this.start(); + + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); } - }; + var id = token; // id can be a string or a number + getToken(); + if (token == "=") { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); + } + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + } else { + parseNodeStatement(graph, id); + } + } /** - * This function cleans the support nodes if they are not needed and adds them when they are. - * - * @param {boolean} [disableStart] - * @private + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph */ - Network.prototype._configureSmoothCurves = function(disableStart) { - if (disableStart === undefined) { - disableStart = true; + function parseSubgraph(graph) { + var subgraph = null; + + // optional subgraph keyword + if (token == "subgraph") { + subgraph = {}; + subgraph.type = "subgraph"; + getToken(); + + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } } - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._createBezierNodes(); - // cleanup unused support nodes - for (var nodeId in this.sectors['support']['nodes']) { - if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) { - if (this.edges[this.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) { - delete this.sectors['support']['nodes'][nodeId]; - } - } - } - } - else { - // delete the support nodes - this.sectors['support']['nodes'] = {}; - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - this.edges[edgeId].via = null; - } - } - } - - - this._updateCalculationNodes(); - if (!disableStart) { - this.moving = true; - this.start(); - } - }; + // open angle bracket + if (token == "{") { + getToken(); - /** - * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but - * are used for the force calculation. - * - * @private - */ - Network.prototype._createBezierNodes = function() { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - var edge = this.edges[edgeId]; - if (edge.via == null) { - var nodeId = "edgeId:".concat(edge.id); - this.sectors['support']['nodes'][nodeId] = new Node( - {id:nodeId, - mass:1, - shape:'circle', - image:"", - internalMultiplier:1 - },{},{},this.constants); - edge.via = this.sectors['support']['nodes'][nodeId]; - edge.via.parentEdgeId = edge.id; - edge.positionBezierNode(); - } - } - } - } - }; - - /** - * load the functions that load the mixins into the prototype. - * - * @private - */ - Network.prototype._initializeMixinLoaders = function () { - for (var mixin in MixinLoader) { - if (MixinLoader.hasOwnProperty(mixin)) { - Network.prototype[mixin] = MixinLoader[mixin]; + if (!subgraph) { + subgraph = {}; } - } - }; + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; - /** - * Load the XY positions of the nodes into the dataset. - */ - Network.prototype.storePosition = function() { - console.log("storePosition is depricated: use .storePositions() from now on.") - this.storePositions(); - }; + // statements + parseStatements(subgraph); - /** - * Load the XY positions of the nodes into the dataset. - */ - Network.prototype.storePositions = function() { - var dataArray = []; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - var allowedToMoveX = !this.nodes.xFixed; - var allowedToMoveY = !this.nodes.yFixed; - if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) { - dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY}); - } + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); } - } - this.nodesData.update(dataArray); - }; + getToken(); - /** - * Return the positions of the nodes. - */ - Network.prototype.getPositions = function(ids) { - var dataArray = {}; - if (ids !== undefined) { - if (Array.isArray(ids) == true) { - for (var i = 0; i < ids.length; i++) { - if (this.nodes[ids[i]] !== undefined) { - var node = this.nodes[ids[i]]; - dataArray[ids[i]] = {x: Math.round(node.x), y: Math.round(node.y)}; - } - } - } - else { - if (this.nodes[ids] !== undefined) { - var node = this.nodes[ids]; - dataArray[ids] = {x: Math.round(node.x), y: Math.round(node.y)}; - } - } - } - else { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - dataArray[nodeId] = {x: Math.round(node.x), y: Math.round(node.y)}; - } + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; + + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; } + graph.subgraphs.push(subgraph); } - return dataArray; - }; - + return subgraph; + } /** - * Center a node in view. - * - * @param {Number} nodeId - * @param {Number} [options] + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. */ - Network.prototype.focusOnNode = function (nodeId, options) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (options === undefined) { - options = {}; - } - var nodePosition = {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y}; - options.position = nodePosition; - options.lockedOnNode = nodeId; - - this.moveTo(options) - } - else { - console.log("This nodeId cannot be found."); - } - }; + function parseAttributeStatement(graph) { + // attribute statements + if (token == "node") { + getToken(); - /** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.scale = Number // scale to move to - * | options.position = {x:Number, y:Number} // position to move to - * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to - */ - Network.prototype.moveTo = function (options) { - if (options === undefined) { - options = {}; - return; - } - if (options.offset === undefined) {options.offset = {x: 0, y: 0}; } - if (options.offset.x === undefined) {options.offset.x = 0; } - if (options.offset.y === undefined) {options.offset.y = 0; } - if (options.scale === undefined) {options.scale = this._getScale(); } - if (options.position === undefined) {options.position = this._getTranslation();} - if (options.animation === undefined) {options.animation = {duration:0}; } - if (options.animation === false ) {options.animation = {duration:0}; } - if (options.animation === true ) {options.animation = {}; } - if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration - if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function + // node attributes + graph.node = parseAttributeList(); + return "node"; + } else if (token == "edge") { + getToken(); - this.animateView(options); - }; + // edge attributes + graph.edge = parseAttributeList(); + return "edge"; + } else if (token == "graph") { + getToken(); - /** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.time = Number // animation time in milliseconds - * | options.scale = Number // scale to animate to - * | options.position = {x:Number, y:Number} // position to animate to - * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, - * // easeInCubic, easeOutCubic, easeInOutCubic, - * // easeInQuart, easeOutQuart, easeInOutQuart, - * // easeInQuint, easeOutQuint, easeInOutQuint - */ - Network.prototype.animateView = function (options) { - if (options === undefined) { - options = {}; - return; + // graph attributes + graph.graph = parseAttributeList(); + return "graph"; } - // release if something focussed on the node - this.releaseNode(); - if (options.locked == true) { - this.lockedOnNodeId = options.lockedOnNode; - this.lockedOnNodeOffset = options.offset; - } + return null; + } - // forcefully complete the old animation if it was still running - if (this.easingTime != 0) { - this._transitionRedraw(1); // by setting easingtime to 1, we finish the animation. + /** + * parse a node statement + * @param {Object} graph + * @param {String | Number} id + */ + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; } + addNode(graph, node); - this.sourceScale = this._getScale(); - this.sourceTranslation = this._getTranslation(); - this.targetScale = options.scale; + // edge statements + parseEdge(graph, id); + } - // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw - // but at least then we'll have the target transition - this._setScale(this.targetScale); - var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - options.position.x, - y: viewCenter.y - options.position.y - }; - this.targetTranslation = { - x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, - y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y - }; + /** + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node + */ + function parseEdge(graph, from) { + while (token == "->" || token == "--") { + var to; + var type = token; + getToken(); - // if the time is set to 0, don't do an animation - if (options.animation.duration == 0) { - if (this.lockedOnNodeId != null) { - this._classicRedraw = this._redraw; - this._redraw = this._lockedRedraw; - } - else { - this._setScale(this.targetScale); - this._setTranslation(this.targetTranslation.x, this.targetTranslation.y); - this._redraw(); + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; + } else { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier or subgraph expected"); + } + to = token; + addNode(graph, { + id: to + }); + getToken(); } - } - else { - this.animating = true; - this.animationSpeed = 1 / (this.renderRefreshRate * options.animation.duration * 0.001) || 1 / this.renderRefreshRate; - this.animationEasingFunction = options.animation.easingFunction; - this._classicRedraw = this._redraw; - this._redraw = this._transitionRedraw; - this._redraw(); - this.start(); - } - }; - /** - * used to animate smoothly by hijacking the redraw function. - * @private - */ - Network.prototype._lockedRedraw = function () { - var nodePosition = {x: this.nodes[this.lockedOnNodeId].x, y: this.nodes[this.lockedOnNodeId].y}; - var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - nodePosition.x, - y: viewCenter.y - nodePosition.y - }; - var sourceTranslation = this._getTranslation(); - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y - }; + // parse edge attributes + var attr = parseAttributeList(); - this._setTranslation(targetTranslation.x,targetTranslation.y); - this._classicRedraw(); - } + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); - Network.prototype.releaseNode = function () { - if (this.lockedOnNodeId != null) { - this._redraw = this._classicRedraw; - this.lockedOnNodeId = null; - this.lockedOnNodeOffset = null; + from = to; } } /** - * - * @param easingTime - * @private + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr */ - Network.prototype._transitionRedraw = function (easingTime) { - this.easingTime = easingTime || this.easingTime + this.animationSpeed; - this.easingTime += this.animationSpeed; + function parseAttributeList() { + var attr = null; - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + while (token == "[") { + getToken(); + attr = {}; + while (token !== "" && token != "]") { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute name expected"); + } + var name = token; - this._setScale(this.sourceScale + (this.targetScale - this.sourceScale) * progress); - this._setTranslation( - this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, - this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress - ); + getToken(); + if (token != "=") { + throw newSyntaxError("Equal sign = expected"); + } + getToken(); - this._classicRedraw(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute value expected"); + } + var value = token; + setValue(attr, name, value); // name can be a path - // cleanup - if (this.easingTime >= 1.0) { - this.animating = false; - this.easingTime = 0; - if (this.lockedOnNodeId != null) { - this._redraw = this._lockedRedraw; + getToken(); + if (token == ",") { + getToken(); + } } - else { - this._redraw = this._classicRedraw; + + if (token != "]") { + throw newSyntaxError("Bracket ] expected"); } - this.emit("animationFinished"); + getToken(); } - }; - Network.prototype._classicRedraw = function () { - // placeholder function to be overloaded by animations; - }; + return attr; + } /** - * Returns true when the Network is active. - * @returns {boolean} + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err */ - Network.prototype.isActive = function () { - return !this.activator || this.activator.active; - }; - + function newSyntaxError(message) { + return new SyntaxError(message + ", got \"" + chop(token, 30) + "\" (char " + index + ")"); + } /** - * Sets the scale - * @returns {Number} + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} */ - Network.prototype.setScale = function () { - return this._setScale(); - }; - + function chop(text, maxLength) { + return text.length <= maxLength ? text : text.substr(0, 27) + "..."; + } /** - * Returns the scale - * @returns {Number} - */ - Network.prototype.getScale = function () { - return this._getScale(); - }; - + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn + */ + function forEach2(array1, array2, fn) { + if (Array.isArray(array1)) { + array1.forEach(function (elem1) { + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); + } else { + fn(elem1, array2); + } + }); + } else { + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); + } else { + fn(array1, array2); + } + } + } /** - * Returns the scale - * @returns {Number} + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData */ - Network.prototype.getCenterCoordinates = function () { - return this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); - }; - + function DOTToGraph(data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; - Network.prototype.getBoundingBox = function(nodeId) { - if (this.nodes[nodeId] !== undefined) { - return this.nodes[nodeId].boundingBox; + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = "image"; + } + graphData.nodes.push(graphNode); + }); } - } - Network.prototype.getConnectedNodes = function(nodeId) { - var nodeList = []; - if (this.nodes[nodeId] !== undefined) { - var node = this.nodes[nodeId]; - var nodeObj = {nodeId : true}; // used to quickly check if node already exists - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - if (edge.toId == nodeId) { - if (nodeObj[edge.fromId] === undefined) { - nodeList.push(edge.fromId); - nodeObj[edge.fromId] = true; - } + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + var convertEdge = function (dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to + }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = dotEdge.type == "->" ? "arrow" : "line"; + return graphEdge; + }; + + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } else { + from = { + id: dotEdge.from + }; } - else if (edge.fromId == nodeId) { - if (nodeObj[edge.toId] === undefined) { - nodeList.push(edge.toId) - nodeObj[edge.toId] = true; - } + + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } else { + to = { + id: dotEdge.to + }; } - } + + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); } - return nodeList; + + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } + + return graphData; } + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; + +/***/ }, +/* 53 */ +/***/ function(module, exports, __webpack_require__) { - Network.prototype.getEdgesFromNode = function(nodeId) { - var edgesList = []; - if (this.nodes[nodeId] !== undefined) { - var node = this.nodes[nodeId]; - for (var i = 0; i < node.edges.length; i++) { - edgesList.push(node.edges[i].id); + "use strict"; + + function parseGephi(gephiJSON, options) { + var edges = []; + var nodes = []; + this.options = { + edges: { + inheritColor: true + }, + nodes: { + allowedToMove: false, + parseColor: false } + }; + + if (options !== undefined) { + this.options.nodes.allowedToMove = options.allowedToMove | false; + this.options.nodes.parseColor = options.parseColor | false; + this.options.edges.inheritColor = options.inheritColor | true; } - return edgesList; - } - Network.prototype.generateColorObject = function(color) { - return util.parseColor(color); + var gEdges = gephiJSON.edges; + var gNodes = gephiJSON.nodes; + for (var i = 0; i < gEdges.length; i++) { + var edge = {}; + var gEdge = gEdges[i]; + edge.id = gEdge.id; + edge.from = gEdge.source; + edge.to = gEdge.target; + edge.attributes = gEdge.attributes; + // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; + // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; + edge.color = gEdge.color; + edge.inheritColor = edge.color !== undefined ? false : this.options.inheritColor; + edges.push(edge); + } - } + for (var i = 0; i < gNodes.length; i++) { + var node = {}; + var gNode = gNodes[i]; + node.id = gNode.id; + node.attributes = gNode.attributes; + node.x = gNode.x; + node.y = gNode.y; + node.label = gNode.label; + if (this.options.nodes.parseColor == true) { + node.color = gNode.color; + } else { + node.color = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; + } + node.radius = gNode.size; + node.allowedToMoveX = this.options.nodes.allowedToMove; + node.allowedToMoveY = this.options.nodes.allowedToMove; + nodes.push(node); + } - module.exports = Network; + return { nodes: nodes, edges: edges }; + } + exports.parseGephi = parseGephi; /***/ }, -/* 52 */ +/* 54 */ /***/ function(module, exports, __webpack_require__) { - /** - * Parse a text source containing data in DOT language into a JSON object. - * The object contains two lists: one with nodes and one with edges. - * - * DOT language reference: http://www.graphviz.org/doc/info/lang.html - * - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graph An object containing two parameters: - * {Object[]} nodes - * {Object[]} edges - */ - function parseDOT (data) { - dot = data; - return parseGraph(); - } - - // token types enumeration - var TOKENTYPE = { - NULL : 0, - DELIMITER : 1, - IDENTIFIER: 2, - UNKNOWN : 3 - }; + "use strict"; - // map with all delimiters - var DELIMITERS = { - '{': true, - '}': true, - '[': true, - ']': true, - ';': true, - '=': true, - ',': true, - - '->': true, - '--': true - }; - - var dot = ''; // current dot file - var index = 0; // current index in dot file - var c = ''; // current token character in expr - var token = ''; // current token - var tokenType = TOKENTYPE.NULL; // type of the token + var util = __webpack_require__(1); /** - * Get the first character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. + * @class Groups + * This class can store groups and properties specific for groups. */ - function first() { - index = 0; - c = dot.charAt(0); + function Groups() { + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + this.useDefaultGroups = true; } - /** - * Get the next character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function next() { - index++; - c = dot.charAt(index); - } /** - * Preview the next character from the dot file. - * @return {String} cNext + * default constants for group colors */ - function nextPreview() { - return dot.charAt(index + 1); - } + Groups.DEFAULT = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint - /** - * Test whether given character is alphabetic or numeric - * @param {String} c - * @return {Boolean} isAlphaNumeric - */ - var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; - function isAlphaNumeric(c) { - return regexAlphaNumeric.test(c); - } + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - /** - * Merge all properties of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge (a, b) { - if (!a) { - a = {}; - } + { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange + { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue + { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green + { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta + { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple + { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue + { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink + { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } - } - return a; - } /** - * Set a value in an object, where the provided parameter name can be a - * path with nested parameters. For example: - * - * var obj = {a: 2}; - * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} - * - * @param {Object} obj - * @param {String} path A parameter name or dot-separated parameter path, - * like "color.highlight.border". - * @param {*} value + * Clear all groups */ - function setValue(obj, path, value) { - var keys = path.split('.'); - var o = obj; - while (keys.length) { - var key = keys.shift(); - if (keys.length) { - // this isn't the end point - if (!o[key]) { - o[key] = {}; + Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function () { + var i = 0; + for (var p in this) { + if (this.hasOwnProperty(p)) { + i++; } - o = o[key]; } - else { - // this is the end point - o[key] = value; - } - } - } + return i; + }; + }; + /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node + * get group properties of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group properties */ - function addNode(graph, node) { - var i, len; - var current = null; - - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; - } - - // find existing node (at root level) by its id - if (root.nodes) { - for (i = 0, len = root.nodes.length; i < len; i++) { - if (node.id === root.nodes[i].id) { - current = root.nodes[i]; - break; - } + Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; } } - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); - } - } + return group; + }; - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + Groups.prototype.add = function (groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; + }; - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); - } - } + module.exports = Groups; + // 20:bright red - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); - } - } +/***/ }, +/* 55 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge + * @class Images + * This class loads images and keeps them stored. */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; - } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes - } + function Images() { + this.images = {}; + this.imageBroken = {}; + this.callback = undefined; } /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; - - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes - } - edge.attr = merge(edge.attr || {}, attr); // merge attributes - - return edge; - } + Images.prototype.setOnloadCallback = function (callback) { + this.callback = callback; + }; /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType + * + * @param {string} url Url of the image + * @param {string} url Url of an image to use if the url image is not found + * @return {Image} img The image object */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ''; - - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } - - do { - var isComment = false; - - // skip comment - if (c == '#') { - // find the previous non-space character - var i = index - 1; - while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { - i--; - } - if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { - // the # is at the start of a line, this is indeed a line comment - while (c != '' && c != '\n') { - next(); - } - isComment = true; + Images.prototype.load = function (url, brokenUrl) { + var img = this.images[url]; // make a pointer + if (img === undefined) { + // create the image + var me = this; + img = new Image(); + img.onload = function () { + // IE11 fix -- thanks dponch! + if (this.width == 0) { + document.body.appendChild(this); + this.width = this.offsetWidth; + this.height = this.offsetHeight; + document.body.removeChild(this); } - } - if (c == '/' && nextPreview() == '/') { - // skip line comment - while (c != '' && c != '\n') { - next(); + + if (me.callback) { + me.images[url] = img; + me.callback(this); } - isComment = true; - } - if (c == '/' && nextPreview() == '*') { - // skip block comment - while (c != '') { - if (c == '*' && nextPreview() == '/') { - // end of block comment found. skip these last two characters - next(); - next(); - break; + }; + + img.onerror = function () { + if (brokenUrl === undefined) { + console.error("Could not load image:", url); + delete this.src; + if (me.callback) { + me.callback(this); } - else { - next(); + } else { + if (me.imageBroken[url] === true) { + if (this.src == brokenUrl) { + console.error("Could not load brokenImage:", brokenUrl); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + me.imageBroken[url] = true; } } - isComment = true; - } + }; - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } + img.src = url; } - while (isComment); - // check for end of dot file - if (c == '') { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; - } + return img; + }; - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); - next(); - return; - } - - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; - } - - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == '-') { - token += c; - next(); + module.exports = Images; - while (isAlphaNumeric(c)) { - token += c; - next(); - } - if (token == 'false') { - token = false; // convert to boolean - } - else if (token == 'true') { - token = true; // convert to boolean - } - else if (!isNaN(Number(token))) { - token = Number(token); // convert to number - } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } +/***/ }, +/* 56 */ +/***/ function(module, exports, __webpack_require__) { - // check for a string enclosed by double quotes - if (c == '"') { - next(); - while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { - token += c; - if (c == '"') { // skip the escape character - next(); - } - next(); - } - if (c != '"') { - throw newSyntaxError('End of string " expected'); - } - next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; - } + "use strict"; - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != '') { - token += c; - next(); - } - throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); - } + var util = __webpack_require__(1); /** - * Parse a graph. - * @returns {Object} graph + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} properties An object containing properties for the node. All + * properties are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square", "icon" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Network.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Network.Groups} grouplist A list with groups. Needed for + * retrieving group properties + * @param {Object} constants An object with default values for + * example for the color + * */ - function parseGraph() { - var graph = {}; - - first(); - getToken(); + function Node(properties, imagelist, grouplist, networkConstants) { + var constants = util.selectiveBridgeObject(["nodes"], networkConstants); + this.options = constants.nodes; - // optional strict keyword - if (token == 'strict') { - graph.strict = true; - getToken(); - } + this.selected = false; + this.hover = false; - // graph or digraph keyword - if (token == 'graph' || token == 'digraph') { - graph.type = token; - getToken(); - } + this.edges = []; // all edges connected to this node - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } + // set defaults for the properties + this.id = undefined; + this.allowedToMoveX = false; + this.allowedToMoveY = false; + this.xFixed = false; + this.yFixed = false; + this.horizontalAlignLeft = true; // these are for the navigation controls + this.verticalAlignTop = true; // these are for the navigation controls + this.baseRadiusValue = networkConstants.nodes.radius; + this.radiusFixed = false; + this.level = -1; + this.preassignedLevel = false; + this.hierarchyEnumerated = false; + this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; - // open angle bracket - if (token != '{') { - throw newSyntaxError('Angle bracket { expected'); - } - getToken(); + this.imagelist = imagelist; + this.grouplist = grouplist; - // statements - parseStatements(graph); + // physics properties + this.x = null; + this.y = null; + this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); - } - getToken(); + // used for reverting to previous position on stabilization + this.previousState = { vx: 0, vy: 0, x: 0, y: 0 }; - // end of file - if (token !== '') { - throw newSyntaxError('End of file expected'); - } - getToken(); + this.fixedData = { x: null, y: null }; - // remove temporary default properties - delete graph.node; - delete graph.edge; - delete graph.graph; + this.setProperties(properties, constants); - return graph; + // variables to tell the node about the network. + this.networkScaleInv = 1; + this.networkScale = 1; + this.canvasTopLeft = { x: -300, y: -300 }; + this.canvasBottomRight = { x: 300, y: 300 }; + this.parentEdgeId = null; } + /** - * Parse a list with statements. - * @param {Object} graph + * Attach a edge to the node + * @param {Edge} edge */ - function parseStatements (graph) { - while (token !== '' && token != '}') { - parseStatement(graph); - if (token == ';') { - getToken(); - } + Node.prototype.attachEdge = function (edge) { + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); } - } + }; /** - * Parse a single statement. Can be a an attribute statement, node - * statement, a series of node statements and edge statements, or a - * parameter. - * @param {Object} graph + * Detach a edge from the node + * @param {Edge} edge */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); - - return; + Node.prototype.detachEdge = function (edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); } + }; - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { + + /** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ + Node.prototype.setProperties = function (properties, constants) { + if (!properties) { return; } + this.properties = properties; - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); + var fields = ["borderWidth", "borderWidthSelected", "shape", "image", "brokenImage", "radius", "fontColor", "fontSize", "fontFace", "fontFill", "fontStrokeWidth", "fontStrokeColor", "group", "mass", "fontDrawThreshold", "scaleFontWithValue", "fontSizeMaxVisible", "customScalingFunction", "iconFontFace", "icon", "iconColor", "iconSize", "value"]; + util.selectiveDeepExtend(fields, this.options, properties); + + // basic properties + if (properties.id !== undefined) { + this.id = properties.id; + } + if (properties.label !== undefined) { + this.label = properties.label;this.originalLabel = properties.label; + } + if (properties.title !== undefined) { + this.title = properties.title; + } + if (properties.x !== undefined) { + this.x = properties.x;this.predefinedPosition = true; + } + if (properties.y !== undefined) { + this.y = properties.y;this.predefinedPosition = true; + } + if (properties.value !== undefined) { + this.value = properties.value; + } + if (properties.level !== undefined) { + this.level = properties.level;this.preassignedLevel = true; } - var id = token; // id can be a string or a number - getToken(); - if (token == '=') { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); - } - graph[id] = token; - getToken(); - // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + // navigation controls properties + if (properties.horizontalAlignLeft !== undefined) { + this.horizontalAlignLeft = properties.horizontalAlignLeft; } - else { - parseNodeStatement(graph, id); + if (properties.verticalAlignTop !== undefined) { + this.verticalAlignTop = properties.verticalAlignTop; + } + if (properties.triggerFunction !== undefined) { + this.triggerFunction = properties.triggerFunction; } - } - /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph - */ - function parseSubgraph (graph) { - var subgraph = null; + if (this.id === undefined) { + throw "Node must have an id"; + } - // optional subgraph keyword - if (token == 'subgraph') { - subgraph = {}; - subgraph.type = 'subgraph'; - getToken(); + // copy group properties + if (typeof properties.group === "number" || typeof properties.group === "string" && properties.group != "") { + var groupObj = this.grouplist.get(properties.group); + util.deepExtend(this.options, groupObj); + // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. + this.options.color = util.parseColor(this.options.color); + } + // individual shape properties + if (properties.radius !== undefined) { + this.baseRadiusValue = this.options.radius; + } + if (properties.color !== undefined) { + this.options.color = util.parseColor(properties.color); + } - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); + if (this.options.image !== undefined && this.options.image != "") { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage); + } else { + throw "No imagelist provided"; } } - // open angle bracket - if (token == '{') { - getToken(); - - if (!subgraph) { - subgraph = {}; - } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; + if (properties.allowedToMoveX !== undefined) { + this.xFixed = !properties.allowedToMoveX; + this.allowedToMoveX = properties.allowedToMoveX; + } else if (properties.x !== undefined && this.allowedToMoveX == false) { + this.xFixed = true; + } - // statements - parseStatements(subgraph); - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); - } - getToken(); + if (properties.allowedToMoveY !== undefined) { + this.yFixed = !properties.allowedToMoveY; + this.allowedToMoveY = properties.allowedToMoveY; + } else if (properties.y !== undefined && this.allowedToMoveY == false) { + this.yFixed = true; + } - // remove temporary default properties - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; + this.radiusFixed = this.radiusFixed || properties.radius !== undefined; - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; - } - graph.subgraphs.push(subgraph); + if (this.options.shape === "image" || this.options.shape === "circularImage") { + this.options.radiusMin = constants.nodes.widthMin; + this.options.radiusMax = constants.nodes.widthMax; } - return subgraph; - } + // choose draw method depending on the shape + switch (this.options.shape) { + case "database": + this.draw = this._drawDatabase;this.resize = this._resizeDatabase;break; + case "box": + this.draw = this._drawBox;this.resize = this._resizeBox;break; + case "circle": + this.draw = this._drawCircle;this.resize = this._resizeCircle;break; + case "ellipse": + this.draw = this._drawEllipse;this.resize = this._resizeEllipse;break; + // TODO: add diamond shape + case "image": + this.draw = this._drawImage;this.resize = this._resizeImage;break; + case "circularImage": + this.draw = this._drawCircularImage;this.resize = this._resizeCircularImage;break; + case "text": + this.draw = this._drawText;this.resize = this._resizeText;break; + case "dot": + this.draw = this._drawDot;this.resize = this._resizeShape;break; + case "square": + this.draw = this._drawSquare;this.resize = this._resizeShape;break; + case "triangle": + this.draw = this._drawTriangle;this.resize = this._resizeShape;break; + case "triangleDown": + this.draw = this._drawTriangleDown;this.resize = this._resizeShape;break; + case "star": + this.draw = this._drawStar;this.resize = this._resizeShape;break; + case "icon": + this.draw = this._drawIcon;this.resize = this._resizeIcon;break; + default: + this.draw = this._drawEllipse;this.resize = this._resizeEllipse;break; + } + // reset the size of the node, this can be changed + this._reset(); + }; /** - * parse an attribute statement like "node [shape=circle fontSize=16]". - * Available keywords are 'node', 'edge', 'graph'. - * The previous list with default attributes will be replaced - * @param {Object} graph - * @returns {String | null} keyword Returns the name of the parsed attribute - * (node, edge, graph), or null if nothing - * is parsed. + * select this node */ - function parseAttributeStatement (graph) { - // attribute statements - if (token == 'node') { - getToken(); - - // node attributes - graph.node = parseAttributeList(); - return 'node'; - } - else if (token == 'edge') { - getToken(); + Node.prototype.select = function () { + this.selected = true; + this._reset(); + }; - // edge attributes - graph.edge = parseAttributeList(); - return 'edge'; - } - else if (token == 'graph') { - getToken(); + /** + * unselect this node + */ + Node.prototype.unselect = function () { + this.selected = false; + this._reset(); + }; - // graph attributes - graph.graph = parseAttributeList(); - return 'graph'; - } - return null; - } + /** + * Reset the calculated size of the node, forces it to recalculate its size + */ + Node.prototype.clearSizeCache = function () { + this._reset(); + }; /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id + * Reset the calculated size of the node, forces it to recalculate its size + * @private */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; - } - addNode(graph, node); + Node.prototype._reset = function () { + this.width = undefined; + this.height = undefined; + }; - // edge statements - parseEdge(graph, id); - } + /** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ + Node.prototype.getTitle = function () { + return typeof this.title === "function" ? this.title() : this.title; + }; /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels */ - function parseEdge(graph, from) { - while (token == '->' || token == '--') { - var to; - var type = token; - getToken(); + Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; - var subgraph = parseSubgraph(graph); - if (subgraph) { - to = subgraph; - } - else { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier or subgraph expected'); - } - to = token; - addNode(graph, { - id: to - }); - getToken(); - } + if (!this.width) { + this.resize(ctx); + } - // parse edge attributes - var attr = parseAttributeList(); + switch (this.options.shape) { + case "circle": + case "dot": + return this.options.radius + borderWidth; - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); + case "ellipse": + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case "box": + case "image": + case "text": + default: + if (this.width) { + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of box + } else { + return 0; + } - from = to; } - } + // TODO: implement calculation of distance to border for all shapes + }; + /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not */ - function parseAttributeList() { - var attr = null; + Node.prototype.isFixed = function () { + return this.xFixed && this.yFixed; + }; - while (token == '[') { - getToken(); - attr = {}; - while (token !== '' && token != ']') { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute name expected'); - } - var name = token; + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + Node.prototype.isSelected = function () { + return this.selected; + }; - getToken(); - if (token != '=') { - throw newSyntaxError('Equal sign = expected'); - } - getToken(); + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + Node.prototype.getValue = function () { + return this.value; + }; - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute value expected'); - } - var value = token; - setValue(attr, name, value); // name can be a path + /** + * Calculate the distance from the nodes location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ + Node.prototype.getDistance = function (x, y) { + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); + }; - getToken(); - if (token ==',') { - getToken(); - } - } - if (token != ']') { - throw newSyntaxError('Bracket ] expected'); + /** + * Adjust the value range of the node. The node will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ + Node.prototype.setValueRange = function (min, max, total) { + if (!this.radiusFixed && this.value !== undefined) { + var scale = this.options.customScalingFunction(min, max, total, this.value); + var radiusDiff = this.options.radiusMax - this.options.radiusMin; + if (this.options.scaleFontWithValue == true) { + var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin; + this.options.fontSize = this.options.fontSizeMin + scale * fontDiff; } - getToken(); + this.options.radius = this.options.radiusMin + scale * radiusDiff; } - return attr; - } + this.baseRadiusValue = this.options.radius; + }; /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - function newSyntaxError(message) { - return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); - } + Node.prototype.draw = function (ctx) { + throw "Draw method not initialized for node"; + }; /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - function chop (text, maxLength) { - return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); - } + Node.prototype.resize = function (ctx) { + throw "Resize method not initialized for node"; + }; /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node */ - function forEach2(array1, array2, fn) { - if (Array.isArray(array1)) { - array1.forEach(function (elem1) { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); - } - else { - fn(elem1, array2); - } - }); - } - else { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } - else { - fn(array1, array2); - } - } - } + 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; + }; - /** - * Convert a string containing a graph in DOT language into a map containing - * with nodes and edges in the format of graph. - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graphData - */ - function DOTToGraph (data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} - }; + Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size - // copy the nodes - if (dotData.nodes) { - dotData.nodes.forEach(function (dotNode) { - var graphNode = { - id: dotNode.id, - label: String(dotNode.label || dotNode.id) - }; - merge(graphNode, dotNode.attr); - if (graphNode.image) { - graphNode.shape = 'image'; + if (!this.width || !this.height) { + // undefined or 0 + var width, height; + if (this.value) { + this.options.radius = this.baseRadiusValue; + var scale = this.imageObj.height / this.imageObj.width; + if (scale !== undefined) { + width = this.options.radius || this.imageObj.width; + height = this.options.radius * scale || this.imageObj.height; + } else { + width = 0; + height = 0; } - graphData.nodes.push(graphNode); - }); - } - - // copy the edges - if (dotData.edges) { - /** - * Convert an edge in DOT format to an edge with VisGraph format - * @param {Object} dotEdge - * @returns {Object} graphEdge - */ - var convertEdge = function (dotEdge) { - var graphEdge = { - from: dotEdge.from, - to: dotEdge.to - }; - merge(graphEdge, dotEdge.attr); - graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; - return graphEdge; + } else { + width = this.imageObj.width; + height = this.imageObj.height; } + this.width = width; + this.height = height; + } + }; - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } - else { - from = { - id: dotEdge.from - } - } - - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } - else { - to = { - id: dotEdge.to - } - } - - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + Node.prototype._drawImageAtPosition = function (ctx) { + if (this.imageObj.width != 0) { + // draw the image + ctx.globalAlpha = 1; + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + } + }; - forEach2(from, to, function (from, to) { - var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); + Node.prototype._drawImageLabel = function (ctx) { + var yLabel; + var offset = 0; - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); - } + if (this.height) { + offset = this.height / 2; + var labelDimensions = this.getTextSize(ctx); - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; + offset += 3; + } } - return graphData; - } - - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + yLabel = this.y + offset; + this._label(ctx, this.label, this.x, yLabel, undefined); + }; -/***/ }, -/* 53 */ -/***/ function(module, exports, __webpack_require__) { + Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - - function parseGephi(gephiJSON, options) { - var edges = []; - var nodes = []; - this.options = { - edges: { - inheritColor: true - }, - nodes: { - allowedToMove: false, - parseColor: false - } - }; + this._drawImageAtPosition(ctx); - if (options !== undefined) { - this.options.nodes['allowedToMove'] = options.allowedToMove | false; - this.options.nodes['parseColor'] = options.parseColor | false; - this.options.edges['inheritColor'] = options.inheritColor | true; - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - var gEdges = gephiJSON.edges; - var gNodes = gephiJSON.nodes; - for (var i = 0; i < gEdges.length; i++) { - var edge = {}; - var gEdge = gEdges[i]; - edge['id'] = gEdge.id; - edge['from'] = gEdge.source; - edge['to'] = gEdge.target; - edge['attributes'] = gEdge.attributes; - // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; - // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; - edge['color'] = gEdge.color; - edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor; - edges.push(edge); - } + this._drawImageLabel(ctx); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + }; - for (var i = 0; i < gNodes.length; i++) { - var node = {}; - var gNode = gNodes[i]; - node['id'] = gNode.id; - node['attributes'] = gNode.attributes; - node['x'] = gNode.x; - node['y'] = gNode.y; - node['label'] = gNode.label; - if (this.options.nodes.parseColor == true) { - node['color'] = gNode.color; + Node.prototype._resizeCircularImage = function (ctx) { + if (!this.imageObj.src || !this.imageObj.width || !this.imageObj.height) { + if (!this.width) { + var diameter = this.options.radius * 2; + this.width = diameter; + this.height = diameter; + this._swapToImageResizeWhenImageLoaded = true; } - else { - node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined; + } else { + if (this._swapToImageResizeWhenImageLoaded) { + this.width = 0; + this.height = 0; + delete this._swapToImageResizeWhenImageLoaded; } - node['radius'] = gNode.size; - node['allowedToMoveX'] = this.options.nodes.allowedToMove; - node['allowedToMoveY'] = this.options.nodes.allowedToMove; - nodes.push(node); + this._resizeImage(ctx); } + }; - return {nodes:nodes, edges:edges}; - } - - exports.parseGephi = parseGephi; + Node.prototype._drawCircularImage = function (ctx) { + this._resizeCircularImage(ctx); -/***/ }, -/* 54 */ -/***/ function(module, exports, __webpack_require__) { + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - var util = __webpack_require__(1); + var centerX = this.left + this.width / 2; + var centerY = this.top + this.height / 2; + var radius = Math.abs(this.height / 2); - /** - * @class Groups - * This class can store groups and properties specific for groups. - */ - function Groups() { - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - this.useDefaultGroups = true; - } + this._drawRawCircle(ctx, centerX, centerY, radius); + ctx.save(); + ctx.circle(this.x, this.y, radius); + ctx.stroke(); + ctx.clip(); - /** - * default constants for group colors - */ - Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // 0: blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // 1: yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // 2: red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // 3: green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // 4: magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // 5: purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // 6: orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // 7: darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // 8: pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}}, // 9: mint + this._drawImageAtPosition(ctx); - {border: "#990000", background: "#EE0000", highlight: {border: "#BB0000", background: "#FF3333"}, hover: {border: "#BB0000", background: "#FF3333"}}, // 10:bright red + ctx.restore(); - {border: "#FF6000", background: "#FF6000", highlight: {border: "#FF6000", background: "#FF6000"}, hover: {border: "#FF6000", background: "#FF6000"}}, // 12: real orange - {border: "#97C2FC", background: "#2B7CE9", highlight: {border: "#D2E5FF", background: "#2B7CE9"}, hover: {border: "#D2E5FF", background: "#2B7CE9"}}, // 13: blue - {border: "#399605", background: "#255C03", highlight: {border: "#399605", background: "#255C03"}, hover: {border: "#399605", background: "#255C03"}}, // 14: green - {border: "#B70054", background: "#FF007E", highlight: {border: "#B70054", background: "#FF007E"}, hover: {border: "#B70054", background: "#FF007E"}}, // 15: magenta - {border: "#AD85E4", background: "#7C29F0", highlight: {border: "#D3BDF0", background: "#7C29F0"}, hover: {border: "#D3BDF0", background: "#7C29F0"}}, // 16: purple - {border: "#4557FA", background: "#000EA1", highlight: {border: "#6E6EFD", background: "#000EA1"}, hover: {border: "#6E6EFD", background: "#000EA1"}}, // 17: darkblue - {border: "#FFC0CB", background: "#FD5A77", highlight: {border: "#FFD1D9", background: "#FD5A77"}, hover: {border: "#FFD1D9", background: "#FD5A77"}}, // 18: pink - {border: "#C2FABC", background: "#74D66A", highlight: {border: "#E6FFE3", background: "#74D66A"}, hover: {border: "#E6FFE3", background: "#74D66A"}}, // 19: mint + this.boundingBox.top = this.y - this.options.radius; + this.boundingBox.left = this.x - this.options.radius; + this.boundingBox.right = this.x + this.options.radius; + this.boundingBox.bottom = this.y + this.options.radius; - {border: "#EE0000", background: "#990000", highlight: {border: "#FF3333", background: "#BB0000"}, hover: {border: "#FF3333", background: "#BB0000"}}, // 20:bright red - ]; + this._drawImageLabel(ctx); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + }; - /** - * Clear all groups - */ - Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function() - { - var i = 0; - for ( var p in this ) { - if (this.hasOwnProperty(p)) { - i++; - } - } - return i; + Node.prototype._resizeBox = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; } }; + Node.prototype._drawBox = function (ctx) { + this._resizeBox(ctx); - /** - * get group properties of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group properties - */ - Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } - else { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; - } - } - - return group; - }; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object - */ - Groups.prototype.add = function (groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; - }; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - module.exports = Groups; + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; -/***/ }, -/* 55 */ -/***/ function(module, exports, __webpack_require__) { + ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius); + ctx.fill(); + ctx.stroke(); - /** - * @class Images - * This class loads images and keeps them stored. - */ - function Images() { - this.images = {}; - this.imageBroken = {}; - this.callback = undefined; - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - /** - * Set an onload callback function. This will be called each time an image - * is loaded - * @param {function} callback - */ - Images.prototype.setOnloadCallback = function(callback) { - this.callback = callback; + this._label(ctx, this.label, this.x, this.y); }; - /** - * - * @param {string} url Url of the image - * @param {string} url Url of an image to use if the url image is not found - * @return {Image} img The image object - */ - Images.prototype.load = function(url, brokenUrl) { - var img = this.images[url]; // make a pointer - if (img === undefined) { - // create the image - var me = this; - img = new Image(); - img.onload = function () { - // IE11 fix -- thanks dponch! - if (this.width == 0) { - document.body.appendChild(this); - this.width = this.offsetWidth; - this.height = this.offsetHeight; - document.body.removeChild(this); - } - - if (me.callback) { - me.images[url] = img; - me.callback(this); - } - }; - - img.onerror = function () { - if (brokenUrl === undefined) { - console.error("Could not load image:", url); - delete this.src; - if (me.callback) { - me.callback(this); - } - } - else { - if (me.imageBroken[url] === true) { - if (this.src == brokenUrl) { - console.error("Could not load brokenImage:", brokenUrl); - delete this.src; - if (me.callback) { - me.callback(this); - } - } - else { - console.error("Could not load image:", url); - this.src = brokenUrl; - } - } - else { - console.error("Could not load image:", url); - this.src = brokenUrl; - me.imageBroken[url] = true; - } - } - }; - img.src = url; + Node.prototype._resizeDatabase = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; } - - return img; }; - module.exports = Images; + Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; -/***/ }, -/* 56 */ -/***/ function(module, exports, __webpack_require__) { + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - var util = __webpack_require__(1); + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + ctx.database(this.x - this.width / 2, this.y - this.height * 0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); - /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} properties An object containing properties for the node. All - * properties are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "icon" - * {string} image An image url - * {string} title An title text, can be HTML - * {anytype} group A group name or number - * @param {Network.Images} imagelist A list with images. Only needed - * when the node has an image - * @param {Network.Groups} grouplist A list with groups. Needed for - * retrieving group properties - * @param {Object} constants An object with default values for - * example for the color - * - */ - function Node(properties, imagelist, grouplist, networkConstants) { - var constants = util.selectiveBridgeObject(['nodes'],networkConstants); - this.options = constants.nodes; + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - this.selected = false; - this.hover = false; + this._label(ctx, this.label, this.x, this.y); + }; - this.edges = []; // all edges connected to this node - this.dynamicEdges = []; - this.reroutedEdges = {}; - // set defaults for the properties - this.id = undefined; - this.allowedToMoveX = false; - this.allowedToMoveY = false; - this.xFixed = false; - this.yFixed = false; - this.horizontalAlignLeft = true; // these are for the navigation controls - this.verticalAlignTop = true; // these are for the navigation controls - this.baseRadiusValue = networkConstants.nodes.radius; - this.radiusFixed = false; - this.level = -1; - this.preassignedLevel = false; - this.hierarchyEnumerated = false; - this.labelDimensions = {top:0, left:0, width:0, height:0, yLine:0}; // could be cached - this.boundingBox = {top:0, left:0, right:0, bottom:0}; + Node.prototype._resizeCircle = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.options.radius = diameter / 2; - this.imagelist = imagelist; - this.grouplist = grouplist; + this.width = diameter; + this.height = diameter; + } + }; - // physics properties - this.fx = 0.0; // external force x - this.fy = 0.0; // external force y - this.vx = 0.0; // velocity x - this.vy = 0.0; // velocity y - this.x = null; - this.y = null; - this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate + Node.prototype._drawRawCircle = function (ctx, x, y, radius) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - // used for reverting to previous position on stabilization - this.previousState = {vx:0,vy:0,x:0,y:0}; + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - this.damping = networkConstants.physics.damping; // written every time gravity is calculated - this.fixedData = {x:null,y:null}; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - this.setProperties(properties, constants); + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(this.x, this.y, radius); + ctx.fill(); + ctx.stroke(); + }; - // creating the variables for clustering - this.resetCluster(); - this.clusterSession = 0; - this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width; - this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height; - this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius; - this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements; - this.growthIndicator = 0; + Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - // variables to tell the node about the network. - this.networkScaleInv = 1; - this.networkScale = 1; - this.canvasTopLeft = {"x": -300, "y": -300}; - this.canvasBottomRight = {"x": 300, "y": 300}; - this.parentEdgeId = null; - } + this._drawRawCircle(ctx, this.x, this.y, this.options.radius); + this.boundingBox.top = this.y - this.options.radius; + this.boundingBox.left = this.x - this.options.radius; + this.boundingBox.right = this.x + this.options.radius; + this.boundingBox.bottom = this.y + this.options.radius; - /** - * Revert the position and velocity of the previous step. - */ - Node.prototype.revertPosition = function() { - this.x = this.previousState.x; - this.y = this.previousState.y; - this.vx = this.previousState.vx; - this.vy = this.previousState.vy; - } + this._label(ctx, this.label, this.x, this.y); + }; + Node.prototype._resizeEllipse = function (ctx) { + if (!this.width) { + var textSize = this.getTextSize(ctx); - /** - * (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 = []; + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; + } + var defaultSize = this.width; + } }; - /** - * Attach a edge to the node - * @param {Edge} edge - */ - 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); - } - }; + Node.prototype._drawEllipse = function (ctx) { + this._resizeEllipse(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - /** - * Detach a edge from the node - * @param {Edge} edge - */ - Node.prototype.detachEdge = function(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); - } - index = this.dynamicEdges.indexOf(edge); - if (index != -1) { - this.dynamicEdges.splice(index, 1); - } - }; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - /** - * Set or overwrite properties for the node - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties - */ - Node.prototype.setProperties = function(properties, constants) { - if (!properties) { - return; - } + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor', - 'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass','fontDrawThreshold', - 'scaleFontWithValue','fontSizeMaxVisible','customScalingFunction','iconFontFace', 'icon', 'iconColor', 'iconSize' - ]; - util.selectiveDeepExtend(fields, this.options, properties); + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - // basic properties - if (properties.id !== undefined) {this.id = properties.id;} - if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;} - if (properties.title !== undefined) {this.title = properties.title;} - if (properties.x !== undefined) {this.x = properties.x; this.predefinedPosition = true;} - if (properties.y !== undefined) {this.y = properties.y; this.predefinedPosition = true;} - if (properties.value !== undefined) {this.value = properties.value;} - if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;} + ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.fill(); + ctx.stroke(); - // navigation controls properties - if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} - if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} - if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;} + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - if (this.id === undefined) { - throw "Node must have an id"; - } + this._label(ctx, this.label, this.x, this.y); + }; - // copy group properties - if (typeof properties.group === 'number' || (typeof properties.group === 'string' && properties.group != '')) { - var groupObj = this.grouplist.get(properties.group); - util.deepExtend(this.options, groupObj); - // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. - this.options.color = util.parseColor(this.options.color); - } - // individual shape properties - if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;} - if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);} + Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, "circle"); + }; - if (this.options.image !== undefined && this.options.image!= "") { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage); - } - else { - throw "No imagelist provided"; - } - } + Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, "triangle"); + }; - if (properties.allowedToMoveX !== undefined) { - this.xFixed = !properties.allowedToMoveX; - this.allowedToMoveX = properties.allowedToMoveX; - } - else if (properties.x !== undefined && this.allowedToMoveX == false) { - this.xFixed = true; - } + Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, "triangleDown"); + }; + Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, "square"); + }; - if (properties.allowedToMoveY !== undefined) { - this.yFixed = !properties.allowedToMoveY; - this.allowedToMoveY = properties.allowedToMoveY; - } - else if (properties.y !== undefined && this.allowedToMoveY == false) { - this.yFixed = true; + Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, "star"); + }; + + Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + this.options.radius = this.baseRadiusValue; + var size = 2 * this.options.radius; + this.width = size; + this.height = size; } + }; - this.radiusFixed = this.radiusFixed || (properties.radius !== undefined); + Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); - if (this.options.shape === 'image' || this.options.shape === 'circularImage') { - this.options.radiusMin = constants.nodes.widthMin; - this.options.radiusMax = constants.nodes.widthMax; - } + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + var radiusMultiplier = 2; // choose draw method depending on the shape - switch (this.options.shape) { - case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; - case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; - case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; - case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; - // TODO: add diamond shape - case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; - case 'circularImage': this.draw = this._drawCircularImage; this.resize = this._resizeCircularImage; break; - case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; - case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; - case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; - case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; - case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; - case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; - case 'icon': this.draw = this._drawIcon; this.resize = this._resizeIcon; break; - default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + 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; } - // reset the size of the node, this can be changed - this._reset(); - }; + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - /** - * select this node - */ - Node.prototype.select = function() { - this.selected = true; - this._reset(); + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](this.x, this.y, this.options.radius); + ctx.fill(); + ctx.stroke(); + + this.boundingBox.top = this.y - this.options.radius; + this.boundingBox.left = this.x - this.options.radius; + this.boundingBox.right = this.x + this.options.radius; + this.boundingBox.bottom = this.y + this.options.radius; + + if (this.label) { + this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, "hanging", true); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + } }; - /** - * unselect this node - */ - Node.prototype.unselect = function() { - this.selected = false; - this._reset(); + Node.prototype._resizeText = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } }; + Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - /** - * Reset the calculated size of the node, forces it to recalculate its size - */ - Node.prototype.clearSizeCache = function() { - this._reset(); - }; + this._label(ctx, this.label, this.x, this.y); - /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private - */ - Node.prototype._reset = function() { - this.width = undefined; - this.height = undefined; + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; }; - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - Node.prototype.getTitle = function() { - return typeof this.title === "function" ? this.title() : this.title; + Node.prototype._resizeIcon = function (ctx) { + if (!this.width) { + var margin = 5; + var iconSize = { + width: Number(this.options.iconSize), + height: Number(this.options.iconSize) + }; + this.width = iconSize.width + 2 * margin; + this.height = iconSize.height + 2 * margin; + } }; - /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels - */ - Node.prototype.distanceToBorder = function (ctx, angle) { - var borderWidth = 1; + Node.prototype._drawIcon = function (ctx) { + this._resizeIcon(ctx); - if (!this.width) { - this.resize(ctx); - } + this.options.iconSize = this.options.iconSize || 50; - switch (this.options.shape) { - case 'circle': - case 'dot': - return this.options.radius+ borderWidth; + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + this._icon(ctx); - case 'ellipse': - var a = this.width / 2; - var b = this.height / 2; - var w = (Math.sin(angle) * a); - var h = (Math.cos(angle) * b); - return a * b / Math.sqrt(w * w + h * h); - // TODO: implement distanceToBorder for database - // TODO: implement distanceToBorder for triangle - // TODO: implement distanceToBorder for triangleDown + this.boundingBox.top = this.y - this.options.iconSize / 2; + this.boundingBox.left = this.x - this.options.iconSize / 2; + this.boundingBox.right = this.x + this.options.iconSize / 2; + this.boundingBox.bottom = this.y + this.options.iconSize / 2; - case 'box': - case 'image': - case 'text': - default: - if (this.width) { - return Math.min( - Math.abs(this.width / 2 / Math.cos(angle)), - Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - // TODO: reckon with border radius too in case of box - } - else { - return 0; - } + if (this.label) { + var iconTextSpacing = 5; + this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, "top", true); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); } - // TODO: implement calculation of distance to border for all shapes - }; - - /** - * Set forces acting on the node - * @param {number} fx Force in horizontal direction - * @param {number} fy Force in vertical direction - */ - Node.prototype._setForce = function(fx, fy) { - this.fx = fx; - this.fy = fy; }; - /** - * Add forces acting on the node - * @param {number} fx Force in horizontal direction - * @param {number} fy Force in vertical direction - * @private - */ - Node.prototype._addForce = function(fx, fy) { - this.fx += fx; - this.fy += fy; - }; + Node.prototype._icon = function (ctx) { + var relativeIconSize = Number(this.options.iconSize) * this.networkScale; - /** - * Store the state before the next step - */ - Node.prototype.storeState = function() { - this.previousState.x = this.x; - this.previousState.y = this.y; - this.previousState.vx = this.vx; - this.previousState.vy = this.vy; - } + if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) { + var iconSize = Number(this.options.iconSize); - /** - * Perform one discrete step for the node - * @param {number} interval Time interval in seconds - */ - Node.prototype.discreteStep = function(interval) { - this.storeState(); - if (!this.xFixed) { - var dx = this.damping * this.vx; // damping force - var ax = (this.fx - dx) / this.options.mass; // acceleration - this.vx += ax * interval; // velocity - this.x += this.vx * interval; // position - } - else { - this.fx = 0; - this.vx = 0; - } + ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace; - if (!this.yFixed) { - var dy = this.damping * this.vy; // damping force - var ay = (this.fy - dy) / this.options.mass; // acceleration - this.vy += ay * interval; // velocity - this.y += this.vy * interval; // position - } - else { - this.fy = 0; - this.vy = 0; + // draw icon + ctx.fillStyle = this.options.iconColor || "black"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.options.icon, this.x, this.y); } }; + Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) { + var relativeFontSize = Number(this.options.fontSize) * this.networkScale; + if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) { + var fontSize = Number(this.options.fontSize); + // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) + if (relativeFontSize >= this.options.fontSizeMaxVisible) { + fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; + } - /** - * Perform one discrete step for the node - * @param {number} interval Time interval in seconds - * @param {number} maxVelocity The speed limit imposed on the velocity - */ - Node.prototype.discreteStepLimited = function(interval, maxVelocity) { - this.storeState(); - if (!this.xFixed) { - var dx = this.damping * this.vx; // damping force - var ax = (this.fx - dx) / this.options.mass; // acceleration - this.vx += ax * interval; // velocity - this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx; - this.x += this.vx * interval; // position - } - else { - this.fx = 0; - this.vx = 0; - } + // fade in when relative scale is between threshold and threshold - 1 + var fontColor = this.options.fontColor || "#000000"; + var strokecolor = this.options.fontStrokeColor; + if (relativeFontSize <= this.options.fontDrawThreshold) { + var opacity = Math.max(0, Math.min(1, 1 - (this.options.fontDrawThreshold - relativeFontSize))); + fontColor = util.overrideOpacity(fontColor, opacity); + strokecolor = util.overrideOpacity(strokecolor, opacity); + } - if (!this.yFixed) { - var dy = this.damping * this.vy; // damping force - var ay = (this.fy - dy) / this.options.mass; // acceleration - this.vy += ay * interval; // velocity - this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy; - this.y += this.vy * interval; // position - } - else { - this.fy = 0; - this.vy = 0; + ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; + + var lines = text.split("\n"); + var lineCount = lines.length; + var yLine = y + (1 - lineCount) / 2 * fontSize; + if (labelUnderNode == true) { + yLine = y + (1 - lineCount) / (2 * fontSize); + } + + // font fill from edges now for nodes! + var width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; + } + var height = fontSize * lineCount; + var left = x - width / 2; + var top = y - height / 2; + if (baseline == "hanging") { + top += 0.5 * fontSize; + top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers + yLine += 4; // distance from node + } + this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; + + // create the fontfill background + if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { + ctx.fillStyle = this.options.fontFill; + ctx.fillRect(left, top, width, height); + } + + // draw text + ctx.fillStyle = fontColor; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; + if (this.options.fontStrokeWidth > 0) { + ctx.lineWidth = this.options.fontStrokeWidth; + ctx.strokeStyle = strokecolor; + ctx.lineJoin = "round"; + } + for (var i = 0; i < lineCount; i++) { + if (this.options.fontStrokeWidth) { + ctx.strokeText(lines[i], x, yLine); + } + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } + } + }; + + + Node.prototype.getTextSize = function (ctx) { + if (this.label !== undefined) { + var fontSize = Number(this.options.fontSize); + if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) { + fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; + } + ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; + + var lines = this.label.split("\n"), + height = (fontSize + 4) * lines.length, + width = 0; + + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); + } + + return { width: width, height: height, lineCount: lines.length }; + } else { + return { width: 0, height: 0, lineCount: 0 }; } }; + /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not + * 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.isFixed = function() { - return (this.xFixed && this.yFixed); + Node.prototype.inArea = function () { + if (this.width !== undefined) { + return this.x + this.width * this.networkScaleInv >= this.canvasTopLeft.x && this.x - this.width * this.networkScaleInv < this.canvasBottomRight.x && this.y + this.height * this.networkScaleInv >= this.canvasTopLeft.y && this.y - this.height * this.networkScaleInv < this.canvasBottomRight.y; + } else { + return true; + } }; + /** - * Check if this node is moving - * @param {number} vmin the minimum velocity considered as "moving" - * @return {boolean} true if moving, false if it has no velocity + * This allows the zoom level of the network to influence the rendering + * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas + * + * @param scale + * @param canvasTopLeft + * @param canvasBottomRight */ - Node.prototype.isMoving = function(vmin) { - var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2)); - // this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2)) - return (velocity > vmin); + Node.prototype.setScaleAndPos = function (scale, canvasTopLeft, canvasBottomRight) { + this.networkScaleInv = 1 / scale; + this.networkScale = scale; + this.canvasTopLeft = canvasTopLeft; + this.canvasBottomRight = canvasBottomRight; }; + /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false + * This allows the zoom level of the network to influence the rendering + * + * @param scale */ - Node.prototype.isSelected = function() { - return this.selected; + Node.prototype.setScale = function (scale) { + this.networkScaleInv = 1 / scale; + this.networkScale = scale; }; + + /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value + * set the velocity at 0. Is called when this node is contained in another during clustering */ - Node.prototype.getValue = function() { - return this.value; + Node.prototype.clearVelocity = function () { + this.vx = 0; + this.vy = 0; }; + /** - * Calculate the distance from the nodes location to the given location (x,y) - * @param {Number} x - * @param {Number} y - * @return {Number} value + * Basic preservation of (kinectic) energy + * + * @param massBeforeClustering */ - Node.prototype.getDistance = function(x, y) { - var dx = this.x - x, - dy = this.y - y; - return Math.sqrt(dx * dx + dy * dy); + Node.prototype.updateVelocity = function (massBeforeClustering) { + var energyBefore = this.vx * this.vx * massBeforeClustering; + //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); + this.vx = Math.sqrt(energyBefore / this.options.mass); + energyBefore = this.vy * this.vy * massBeforeClustering; + //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); + this.vy = Math.sqrt(energyBefore / this.options.mass); }; + module.exports = Node; + +/***/ }, +/* 57 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var Node = __webpack_require__(56); /** - * Adjust the value range of the node. The node will adjust it's radius - * based on its value. - * @param {Number} min - * @param {Number} max + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color */ - Node.prototype.setValueRange = function(min, max, total) { - if (!this.radiusFixed && this.value !== undefined) { - var scale = this.options.customScalingFunction(min, max, total, this.value); - var radiusDiff = this.options.radiusMax - this.options.radiusMin; - if (this.options.scaleFontWithValue == true) { - var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin; - this.options.fontSize = this.options.fontSizeMin + scale * fontDiff; - } - this.options.radius = this.options.radiusMin + scale * radiusDiff; + function Edge(properties, body, networkConstants) { + if (body === undefined) { + throw "No body provided"; } + var fields = ["edges"]; + var constants = util.selectiveBridgeObject(fields, networkConstants); + this.options = constants.edges; - this.baseRadiusValue = this.options.radius; - }; + this.options.smoothCurves = networkConstants.smoothCurves; + this.body = body; - /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - Node.prototype.draw = function(ctx) { - throw "Draw method not initialized for node"; - }; + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.title = undefined; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + this.dirtyLabel = true; + this.colorDirty = true; - /** - * Recalculate the size of this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - Node.prototype.resize = function(ctx) { - throw "Resize method not initialized for node"; - }; + this.from = null; // a node + this.to = null; // a node + this.via = null; // a temp node + + this.fromBackup = null; // used to clean up after reconnect (used for manipulation) + this.toBackup = null; // used to clean up after reconnect (used for manipulation) + + // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster + // by storing the original information we can revert to the original connection when the cluser is opened. + this.fromArray = []; + this.toArray = []; + + this.connected = false; + + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties); + + this.controlNodesEnabled = false; + this.controlNodes = { from: null, to: null, positions: {} }; + this.connectedNode = null; + } /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties */ - 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); - }; + Edge.prototype.setProperties = function (properties) { + this.colorDirty = true; + if (!properties) { + return; + } + this.properties = properties; - Node.prototype._resizeImage = function (ctx) { - // TODO: pre calculate the image size + var fields = ["style", "fontSize", "fontFace", "fontColor", "fontFill", "fontStrokeWidth", "fontStrokeColor", "width", "widthSelectionMultiplier", "hoverWidth", "arrowScaleFactor", "dash", "inheritColor", "labelAlignment", "opacity", "customScalingFunction", "useGradients", "value"]; + util.selectiveDeepExtend(fields, this.options, properties); - if (!this.width || !this.height) { // undefined or 0 - var width, height; - if (this.value) { - this.options.radius= this.baseRadiusValue; - var scale = this.imageObj.height / this.imageObj.width; - if (scale !== undefined) { - width = this.options.radius|| this.imageObj.width; - height = this.options.radius* scale || this.imageObj.height; + 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.label !== undefined) { + this.label = properties.label;this.dirtyLabel = true; + } + + if (properties.title !== undefined) { + this.title = properties.title; + } + if (properties.value !== undefined) { + this.value = properties.value; + } + if (properties.length !== undefined) { + this.physics.springLength = properties.length; + } + + if (properties.color !== undefined) { + this.options.inheritColor = false; + if (util.isString(properties.color)) { + this.options.color.color = properties.color; + this.options.color.highlight = properties.color; + } else { + if (properties.color.color !== undefined) { + this.options.color.color = properties.color.color; } - else { - width = 0; - height = 0; + if (properties.color.highlight !== undefined) { + this.options.color.highlight = properties.color.highlight; + } + if (properties.color.hover !== undefined) { + this.options.color.hover = properties.color.hover; } - } - else { - width = this.imageObj.width; - height = this.imageObj.height; - } - this.width = width; - this.height = height; - - this.growthIndicator = 0; - if (this.width > 0 && this.height > 0) { - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - width; } } - }; - Node.prototype._drawImageAtPosition = function (ctx) { - if (this.imageObj.width != 0 ) { - // draw the shade - if (this.clusterSize > 1) { - var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0); - lineWidth *= this.networkScaleInv; - lineWidth = Math.min(0.2 * this.width,lineWidth); + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); - ctx.globalAlpha = 0.5; - ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); - } + this.widthFixed = this.widthFixed || properties.width !== undefined; + this.lengthFixed = this.lengthFixed || properties.length !== undefined; - // draw the image - ctx.globalAlpha = 1.0; - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); - } - }; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - Node.prototype._drawImageLabel = function (ctx) { - var yLabel; - var offset = 0; - - if (this.height){ - offset = this.height / 2; - var labelDimensions = this.getTextSize(ctx); - - if (labelDimensions.lineCount >= 1){ - offset += labelDimensions.height / 2; - offset += 3; - } + // set draw method based on style + switch (this.options.style) { + case "line": + this.draw = this._drawLine;break; + case "arrow": + this.draw = this._drawArrow;break; + case "arrow-center": + this.draw = this._drawArrowCenter;break; + case "dash-line": + this.draw = this._drawDashLine;break; + default: + this.draw = this._drawLine;break; } - - yLabel = this.y + offset; - - this._label(ctx, this.label, this.x, yLabel, undefined); }; - Node.prototype._drawImage = function (ctx) { - this._resizeImage(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - this._drawImageAtPosition(ctx); - - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - - this._drawImageLabel(ctx); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); - }; + /** + * Connect an edge to its nodes + */ + Edge.prototype.connect = function () { + this.disconnect(); - Node.prototype._resizeCircularImage = function (ctx) { - if(!this.imageObj.src || !this.imageObj.width || !this.imageObj.height){ - if (!this.width) { - var diameter = this.options.radius * 2; - this.width = diameter; - this.height = diameter; + this.from = this.body.nodes[this.fromId] || null; + this.to = this.body.nodes[this.toId] || null; + this.connected = this.from !== null && this.to !== null; - // scaling used for clustering - //this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - //this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.options.radius- 0.5*diameter; - this._swapToImageResizeWhenImageLoaded = true; + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); } - } - else { - if (this._swapToImageResizeWhenImageLoaded) { - this.width = 0; - this.height = 0; - delete this._swapToImageResizeWhenImageLoaded; + if (this.to) { + this.to.detachEdge(this); } - this._resizeImage(ctx); } - - }; - - Node.prototype._drawCircularImage = function (ctx) { - this._resizeCircularImage(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var centerX = this.left + (this.width / 2); - var centerY = this.top + (this.height / 2); - var radius = Math.abs(this.height / 2); - - this._drawRawCircle(ctx, centerX, centerY, radius); - - ctx.save(); - ctx.circle(this.x, this.y, radius); - ctx.stroke(); - ctx.clip(); - - this._drawImageAtPosition(ctx); - - ctx.restore(); - - this.boundingBox.top = this.y - this.options.radius; - this.boundingBox.left = this.x - this.options.radius; - this.boundingBox.right = this.x + this.options.radius; - this.boundingBox.bottom = this.y + this.options.radius; - - this._drawImageLabel(ctx); - - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); }; - Node.prototype._resizeBox = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.growthIndicator = this.width - (textSize.width + 2 * margin); - // this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - + /** + * Disconnect an edge from its nodes + */ + Edge.prototype.disconnect = function () { + if (this.from) { + this.from.detachEdge(this); + this.from = null; } - }; - - Node.prototype._drawBox = function (ctx) { - this._resizeBox(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius); - ctx.stroke(); + if (this.to) { + this.to.detachEdge(this); + this.to = null; } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - - ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius); - ctx.fill(); - ctx.stroke(); - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + this.connected = false; + }; - this._label(ctx, this.label, this.x, this.y); + /** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ + Edge.prototype.getTitle = function () { + return typeof this.title === "function" ? this.title() : this.title; }; - Node.prototype._resizeDatabase = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + Edge.prototype.getValue = function () { + return this.value; + }; - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - size; + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + */ + Edge.prototype.setValueRange = function (min, max, total) { + if (!this.widthFixed && this.value !== undefined) { + var scale = this.options.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.widthMax - this.options.widthMin; + this.options.width = this.options.widthMin + scale * widthDiff; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; } }; - Node.prototype._drawDatabase = function (ctx) { - this._resizeDatabase(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + /** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + Edge.prototype.draw = function (ctx) { + throw "Method draw not initialized in edge"; + }; - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ + Edge.prototype.isOverlappingWith = function (obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - 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(); + return dist < distMax; + } else { + return false; } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); - - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - - this._label(ctx, this.label, this.x, this.y); }; + Edge.prototype._getColor = function (ctx) { + var colorObj = this.options.color; + if (this.options.useGradients == true) { + var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); + var fromColor, toColor; + fromColor = this.from.options.color.highlight.border; + toColor = this.to.options.color.highlight.border; - Node.prototype._resizeCircle = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.radius = diameter / 2; - this.width = diameter; - this.height = diameter; + if (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity); + } else if (this.from.selected == true && this.to.selected == false) { + toColor = this.to.options.color.border; + } else if (this.from.selected == false && this.to.selected == true) { + fromColor = this.from.options.color.border; + } + grd.addColorStop(0, fromColor); + grd.addColorStop(1, toColor); + return grd; + } - // scaling used for clustering - // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.options.radius- 0.5*diameter; + if (this.colorDirty === true) { + if (this.options.inheritColor == "to") { + colorObj = { + highlight: this.to.options.color.highlight.border, + hover: this.to.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { + colorObj = { + highlight: this.from.options.color.highlight.border, + hover: this.from.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } + this.options.color = colorObj; + this.colorDirty = false; } - }; - Node.prototype._drawRawCircle = function (ctx, x, y, radius) { - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - ctx.circle(x, y, radius+2*ctx.lineWidth); - ctx.stroke(); + if (this.selected == true) { + return colorObj.highlight; + } else if (this.hover == true) { + return colorObj.hover; + } else { + return colorObj.color; } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(this.x, this.y, radius); - ctx.fill(); - ctx.stroke(); }; - Node.prototype._drawCircle = function (ctx) { - this._resizeCircle(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - this._drawRawCircle(ctx, this.x, this.y, this.options.radius); - - this.boundingBox.top = this.y - this.options.radius; - this.boundingBox.left = this.x - this.options.radius; - this.boundingBox.right = this.x + this.options.radius; - this.boundingBox.bottom = this.y + this.options.radius; - - this._label(ctx, this.label, this.x, this.y); - }; - - Node.prototype._resizeEllipse = function (ctx) { - if (!this.width) { - var textSize = this.getTextSize(ctx); - - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } - var defaultSize = this.width; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - defaultSize; - } - }; - - Node.prototype._drawEllipse = function (ctx) { - this._resizeEllipse(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - 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 : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); - - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - - this._label(ctx, this.label, this.x, this.y); - }; - - Node.prototype._drawDot = function (ctx) { - this._drawShape(ctx, 'circle'); - }; - - Node.prototype._drawTriangle = function (ctx) { - this._drawShape(ctx, 'triangle'); - }; - - Node.prototype._drawTriangleDown = function (ctx) { - this._drawShape(ctx, 'triangleDown'); - }; - - Node.prototype._drawSquare = function (ctx) { - this._drawShape(ctx, 'square'); - }; - - Node.prototype._drawStar = function (ctx) { - this._drawShape(ctx, 'star'); - }; - - Node.prototype._resizeShape = function (ctx) { - if (!this.width) { - this.options.radius= this.baseRadiusValue; - var size = 2 * this.options.radius; - this.width = size; - this.height = size; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - size; - } - }; - - Node.prototype._drawShape = function (ctx, shape) { - this._resizeShape(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - 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.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](this.x, this.y, this.options.radius); - ctx.fill(); - ctx.stroke(); - - this.boundingBox.top = this.y - this.options.radius; - this.boundingBox.left = this.x - this.options.radius; - this.boundingBox.right = this.x + this.options.radius; - this.boundingBox.bottom = this.y + this.options.radius; - - if (this.label) { - this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'hanging',true); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); - } - }; - - Node.prototype._resizeText = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - (textSize.width + 2 * margin); - } - }; - - Node.prototype._drawText = function (ctx) { - this._resizeText(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - this._label(ctx, this.label, this.x, this.y); - - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - }; - - Node.prototype._resizeIcon = function (ctx) { - if (!this.width) { - var margin = 5; - var iconSize = - { - width: Number(this.options.iconSize), - height: Number(this.options.iconSize) - }; - this.width = iconSize.width + 2 * margin; - this.height = iconSize.height + 2 * margin; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - (iconSize.width + 2 * margin); - } - }; - - Node.prototype._drawIcon = function (ctx) { - this._resizeIcon(ctx); - - this.options.iconSize = this.options.iconSize || 50; - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - this._icon(ctx); - - - this.boundingBox.top = this.y - this.options.iconSize/2; - this.boundingBox.left = this.x - this.options.iconSize/2; - this.boundingBox.right = this.x + this.options.iconSize/2; - this.boundingBox.bottom = this.y + this.options.iconSize/2; - - if (this.label) { - var iconTextSpacing = 5; - this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, 'top', true); - - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); - } - }; - - Node.prototype._icon = function (ctx) { - var relativeIconSize = Number(this.options.iconSize) * this.networkScale; - - if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) { - - var iconSize = Number(this.options.iconSize); - - ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace; - - // draw icon - ctx.fillStyle = this.options.iconColor || "black"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.options.icon, this.x, this.y); - } - }; - - Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) { - var relativeFontSize = Number(this.options.fontSize) * this.networkScale; - if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) { - var fontSize = Number(this.options.fontSize); - - // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) - if (relativeFontSize >= this.options.fontSizeMaxVisible) { - fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; - } - - // fade in when relative scale is between threshold and threshold - 1 - var fontColor = this.options.fontColor || "#000000"; - var strokecolor = this.options.fontStrokeColor; - if (relativeFontSize <= this.options.fontDrawThreshold) { - var opacity = Math.max(0,Math.min(1,1 - (this.options.fontDrawThreshold - relativeFontSize))); - fontColor = util.overrideOpacity(fontColor, opacity); - strokecolor = util.overrideOpacity(strokecolor, opacity); - - } - - ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; - - var lines = text.split('\n'); - var lineCount = lines.length; - var yLine = y + (1 - lineCount) / 2 * fontSize; - if (labelUnderNode == true) { - yLine = y + (1 - lineCount) / (2 * fontSize); - } - - // font fill from edges now for nodes! - var width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; - } - var height = fontSize * lineCount; - var left = x - width / 2; - var top = y - height / 2; - if (baseline == "hanging") { - top += 0.5 * fontSize; - top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers - yLine += 4; // distance from node - } - this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine}; - - // create the fontfill background - if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { - ctx.fillStyle = this.options.fontFill; - ctx.fillRect(left, top, width, height); - } - - // draw text - ctx.fillStyle = fontColor; - ctx.textAlign = align || "center"; - ctx.textBaseline = baseline || "middle"; - if (this.options.fontStrokeWidth > 0){ - ctx.lineWidth = this.options.fontStrokeWidth; - ctx.strokeStyle = strokecolor; - ctx.lineJoin = 'round'; - } - for (var i = 0; i < lineCount; i++) { - if(this.options.fontStrokeWidth){ - ctx.strokeText(lines[i], x, yLine); - } - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } - } - }; - - - Node.prototype.getTextSize = function(ctx) { - if (this.label !== undefined) { - var fontSize = Number(this.options.fontSize); - if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) { - fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; - } - ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; - - var lines = this.label.split('\n'), - height = (fontSize + 4) * lines.length, - width = 0; - - for (var i = 0, iMax = lines.length; i < iMax; i++) { - width = Math.max(width, ctx.measureText(lines[i]).width); - } - - return {"width": width, "height": height, lineCount: lines.length}; - } - else { - return {"width": 0, "height": 0, lineCount: 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.networkScaleInv >= this.canvasTopLeft.x && - this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x && - this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y && - this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y); - } - else { - return true; - } - }; - - /** - * 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 network to influence the rendering - * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas - * - * @param scale - * @param canvasTopLeft - * @param canvasBottomRight - */ - Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { - this.networkScaleInv = 1.0/scale; - this.networkScale = scale; - this.canvasTopLeft = canvasTopLeft; - this.canvasBottomRight = canvasBottomRight; - }; - - - /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale - */ - Node.prototype.setScale = function(scale) { - this.networkScaleInv = 1.0/scale; - this.networkScale = scale; - }; - - - - /** - * 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 = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); - this.vx = Math.sqrt(energyBefore/this.options.mass); - energyBefore = this.vy * this.vy * massBeforeClustering; - //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); - this.vy = Math.sqrt(energyBefore/this.options.mass); - }; - - module.exports = Node; - - -/***/ }, -/* 57 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var Node = __webpack_require__(56); - - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with properties. Must contain - * At least properties from and to. - * Available properties: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color - */ - function Edge (properties, network, networkConstants) { - if (!network) { - throw "No network provided"; - } - var fields = ['edges','physics']; - var constants = util.selectiveBridgeObject(fields,networkConstants); - this.options = constants.edges; - this.physics = constants.physics; - this.options['smoothCurves'] = networkConstants['smoothCurves']; - - - this.network = network; - - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.title = undefined; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached - this.dirtyLabel = true; - this.colorDirty = true; - - this.from = null; // a node - this.to = null; // a node - this.via = null; // a temp node - - this.fromBackup = null; // used to clean up after reconnect - this.toBackup = null;; // used to clean up after reconnect - - // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster - // by storing the original information we can revert to the original connection when the cluser is opened. - this.originalFromId = []; - this.originalToId = []; - - this.connected = false; - - this.widthFixed = false; - this.lengthFixed = false; - - this.setProperties(properties); - - this.controlNodesEnabled = false; - this.controlNodes = {from:null, to:null, positions:{}}; - this.connectedNode = null; - } - - /** - * Set or overwrite properties for the edge - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties - */ - Edge.prototype.setProperties = function(properties) { - this.colorDirty = true; - if (!properties) { - return; - } - - var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width', - 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity', - 'customScalingFunction','useGradients' - ]; - util.selectiveDeepExtend(fields, this.options, properties); - - 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.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;} - - if (properties.title !== undefined) {this.title = properties.title;} - if (properties.value !== undefined) {this.value = properties.value;} - if (properties.length !== undefined) {this.physics.springLength = properties.length;} - - if (properties.color !== undefined) { - this.options.inheritColor = false; - if (util.isString(properties.color)) { - this.options.color.color = properties.color; - this.options.color.highlight = properties.color; - } - else { - if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;} - if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;} - if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;} - } - } - - - - // 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.widthSelected = this.options.width* this.options.widthSelectionMultiplier; - - // set draw method based on style - switch (this.options.style) { - case 'line': this.draw = this._drawLine; break; - case 'arrow': this.draw = this._drawArrow; break; - case 'arrow-center': this.draw = this._drawArrowCenter; break; - case 'dash-line': this.draw = this._drawDashLine; break; - default: this.draw = this._drawLine; break; - } - }; - - - /** - * Connect an edge to its nodes - */ - Edge.prototype.connect = function () { - this.disconnect(); - - this.from = this.network.nodes[this.fromId] || null; - this.to = this.network.nodes[this.toId] || null; - this.connected = (this.from && this.to); - - if (this.connected) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } - else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); - } - } - }; - - /** - * Disconnect an edge from its nodes - */ - Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; - } - - this.connected = false; - }; - - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - Edge.prototype.getTitle = function() { - return typeof this.title === "function" ? this.title() : this.title; - }; - - - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - Edge.prototype.getValue = function() { - return this.value; - }; - - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - */ - Edge.prototype.setValueRange = function(min, max, total) { - if (!this.widthFixed && this.value !== undefined) { - var scale = this.options.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.widthMax - this.options.widthMin; - this.options.width = this.options.widthMin + scale * widthDiff; - this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; - } - }; - - /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - Edge.prototype.draw = function(ctx) { - throw "Method draw not initialized in edge"; - }; - - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge - */ - Edge.prototype.isOverlappingWith = function(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; - - var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - - return (dist < distMax); - } - else { - return false - } - }; - - Edge.prototype._getColor = function(ctx) { - var colorObj = this.options.color; - if (this.options.useGradients == true) { - var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); - var fromColor, toColor; - fromColor = this.from.options.color.highlight.border; - toColor = this.to.options.color.highlight.border; - - - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity); - } - else if (this.from.selected == true && this.to.selected == false) { - toColor = this.to.options.color.border; - } - else if (this.from.selected == false && this.to.selected == true) { - fromColor = this.from.options.color.border; - } - grd.addColorStop(0, fromColor); - grd.addColorStop(1, toColor); - return grd; - } - - if (this.colorDirty === true) { - if (this.options.inheritColor == "to") { - colorObj = { - highlight: this.to.options.color.highlight.border, - hover: this.to.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; - } - else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { - colorObj = { - highlight: this.from.options.color.highlight.border, - hover: this.from.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; - } - this.options.color = colorObj; - this.colorDirty = false; - } - - - - if (this.selected == true) {return colorObj.highlight;} - else if (this.hover == true) {return colorObj.hover;} - else {return colorObj.color;} - }; - - - /** - * Redraw a edge as a line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._drawLine = function(ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); + /** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawLine = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); if (this.from != this.to) { // draw line @@ -28564,17 +26651,15 @@ return /******/ (function(modules) { // webpackBootstrap var point; if (this.label) { if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { point = this._pointOnLine(0.5); } this._label(ctx, this.label, point.x, point.y); } - } - else { + } else { var x, y; var radius = this.physics.springLength / 4; var node = this.from; @@ -28584,8 +26669,7 @@ return /******/ (function(modules) { // webpackBootstrap if (node.width > node.height) { x = node.x + node.width / 2; y = node.y - radius; - } - else { + } else { x = node.x + radius; y = node.y - node.height / 2; } @@ -28601,52 +26685,45 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Number} width * @private */ - Edge.prototype._getLineWidth = function() { + Edge.prototype._getLineWidth = function () { if (this.selected == true) { - return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv); - } - else { + return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv); + } else { if (this.hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv); - } - else { - return Math.max(this.options.width, 0.3*this.networkScaleInv); + return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv); + } else { + return Math.max(this.options.width, 0.3 * this.networkScaleInv); } } }; Edge.prototype._getViaCoordinates = function () { - if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) { + if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) { return this.via; - } - else if (this.options.smoothCurves.enabled == false) { - return {x:0,y:0}; - } - else { + } else if (this.options.smoothCurves.enabled == false) { + return { x: 0, y: 0 }; + } else { var xVia = null; var yVia = null; var factor = this.options.smoothCurves.roundness; var type = this.options.smoothCurves.type; var dx = Math.abs(this.from.x - this.to.x); var dy = Math.abs(this.from.y - this.to.y); - if (type == 'discrete' || type == 'diagonalCross') { + if (type == "discrete" || type == "diagonalCross") { if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y - factor * dy; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y - factor * dy; } - } - else if (this.from.y < this.to.y) { + } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y + factor * dy; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y + factor * dy; } @@ -28654,24 +26731,20 @@ return /******/ (function(modules) { // webpackBootstrap if (type == "discrete") { xVia = dx < factor * dy ? this.from.x : xVia; } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y - factor * dx; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y - factor * dx; } - } - else if (this.from.y < this.to.y) { + } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y + factor * dx; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y + factor * dx; } @@ -28680,116 +26753,101 @@ return /******/ (function(modules) { // webpackBootstrap yVia = dy < factor * dx ? this.from.y : yVia; } } - } - else if (type == "straightCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down + } else if (type == "straightCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + // up - down xVia = this.from.x; if (this.from.y < this.to.y) { yVia = this.to.y - (1 - factor) * dy; - } - else { + } else { yVia = this.to.y + (1 - factor) * dy; } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + // left - right if (this.from.x < this.to.x) { xVia = this.to.x - (1 - factor) * dx; - } - else { + } else { xVia = this.to.x + (1 - factor) * dx; } yVia = this.from.y; } - } - else if (type == 'horizontal') { + } else if (type == "horizontal") { if (this.from.x < this.to.x) { xVia = this.to.x - (1 - factor) * dx; - } - else { + } else { xVia = this.to.x + (1 - factor) * dx; } yVia = this.from.y; - } - else if (type == 'vertical') { + } else if (type == "vertical") { xVia = this.from.x; if (this.from.y < this.to.y) { yVia = this.to.y - (1 - factor) * dy; - } - else { + } else { yVia = this.to.y + (1 - factor) * dy; } - } - else if (type == 'curvedCW') { + } else if (type == "curvedCW") { var dx = this.to.x - this.from.x; var dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx*dx + dy*dy); + var radius = Math.sqrt(dx * dx + dy * dy); var pi = Math.PI; - var originalAngle = Math.atan2(dy,dx); - var myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi); + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); - xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle); - yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle); - } - else if (type == 'curvedCCW') { + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else if (type == "curvedCCW") { var dx = this.to.x - this.from.x; var dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx*dx + dy*dy); + var radius = Math.sqrt(dx * dx + dy * dy); var pi = Math.PI; - var originalAngle = Math.atan2(dy,dx); - var myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi); + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); - xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle); - yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle); - } - else { // continuous + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else { + // continuous if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y - factor * dy; xVia = this.to.x < xVia ? this.to.x : xVia; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y - factor * dy; xVia = this.to.x > xVia ? this.to.x : xVia; } - } - else if (this.from.y < this.to.y) { + } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dy; yVia = this.from.y + factor * dy; xVia = this.to.x < xVia ? this.to.x : xVia; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dy; yVia = this.from.y + factor * dy; xVia = this.to.x > xVia ? this.to.x : xVia; } } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { if (this.from.y > this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y - factor * dx; yVia = this.to.y > yVia ? this.to.y : yVia; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y - factor * dx; yVia = this.to.y > yVia ? this.to.y : yVia; } - } - else if (this.from.y < this.to.y) { + } else if (this.from.y < this.to.y) { if (this.from.x < this.to.x) { xVia = this.from.x + factor * dx; yVia = this.from.y + factor * dx; yVia = this.to.y < yVia ? this.to.y : yVia; - } - else if (this.from.x > this.to.x) { + } else if (this.from.x > this.to.x) { xVia = this.from.x - factor * dx; yVia = this.from.y + factor * dx; yVia = this.to.y < yVia ? this.to.y : yVia; @@ -28799,7 +26857,7 @@ return /******/ (function(modules) { // webpackBootstrap } - return {x: xVia, y: yVia}; + return { x: xVia, y: yVia }; } }; @@ -28819,24 +26877,21 @@ return /******/ (function(modules) { // webpackBootstrap ctx.lineTo(this.to.x, this.to.y); ctx.stroke(); return null; - } - else { - // this.via.x = via.x; - // this.via.y = via.y; - ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); + } else { + // this.via.x = via.x; + // this.via.y = via.y; + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); ctx.stroke(); //ctx.circle(via.x,via.y,2) //ctx.stroke(); return via; } - } - else { - ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); + } else { + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); ctx.stroke(); return this.via; } - } - else { + } else { ctx.lineTo(this.to.x, this.to.y); ctx.stroke(); return null; @@ -28868,12 +26923,11 @@ return /******/ (function(modules) { // webpackBootstrap */ Edge.prototype._label = function (ctx, text, x, y) { if (text) { - ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + - this.options.fontSize + "px " + this.options.fontFace; + ctx.font = (this.from.selected || this.to.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; var yLine; if (this.dirtyLabel == true) { - var lines = String(text).split('\n'); + var lines = String(text).split("\n"); var lineCount = lines.length; var fontSize = Number(this.options.fontSize); yLine = y + (1 - lineCount) / 2 * fontSize; @@ -28888,25 +26942,25 @@ return /******/ (function(modules) { // webpackBootstrap var top = y - height / 2; // cache - this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine}; - } - - var yLine = this.labelDimensions.yLine; - - ctx.save(); - - if (this.options.labelAlignment != "horizontal"){ - ctx.translate(x, yLine); - this._rotateForLabelAlignment(ctx); - x = 0; - yLine = 0; - } + this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; + } - - this._drawLabelRect(ctx); - this._drawLabelText(ctx,x,yLine, lines, lineCount, fontSize); - - ctx.restore(); + var yLine = this.labelDimensions.yLine; + + ctx.save(); + + if (this.options.labelAlignment != "horizontal") { + ctx.translate(x, yLine); + this._rotateForLabelAlignment(ctx); + x = 0; + yLine = 0; + } + + + this._drawLabelRect(ctx); + this._drawLabelText(ctx, x, yLine, lines, lineCount, fontSize); + + ctx.restore(); } }; @@ -28915,17 +26969,17 @@ return /******/ (function(modules) { // webpackBootstrap * @param {CanvasRenderingContext2D} ctx * @private */ - Edge.prototype._rotateForLabelAlignment = function(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + Edge.prototype._rotateForLabelAlignment = function (ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); - // rotate so label it is readable - if((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)){ - angleInDegrees = angleInDegrees + Math.PI; - } - - ctx.rotate(angleInDegrees); + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; + } + + ctx.rotate(angleInDegrees); }; /** @@ -28934,22 +26988,19 @@ return /******/ (function(modules) { // webpackBootstrap * @param {String} labelAlignment * @private */ - Edge.prototype._drawLabelRect = function(ctx) { - if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { - ctx.fillStyle = this.options.fontFill; - - var lineMargin = 2; + Edge.prototype._drawLabelRect = function (ctx) { + if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { + ctx.fillStyle = this.options.fontFill; + + var lineMargin = 2; - if (this.options.labelAlignment == 'line-center') { + if (this.options.labelAlignment == "line-center") { ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); - } - else if (this.options.labelAlignment == 'line-above') { + } else if (this.options.labelAlignment == "line-above") { ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); - } - else if (this.options.labelAlignment == 'line-below') { + } else if (this.options.labelAlignment == "line-below") { ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); - } - else { + } else { ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); } } @@ -28965,43 +27016,40 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Number} fontSize * @private */ - Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSize) { - // draw text - ctx.fillStyle = this.options.fontColor || "black"; - ctx.textAlign = "center"; + Edge.prototype._drawLabelText = function (ctx, x, yLine, lines, lineCount, fontSize) { + // draw text + ctx.fillStyle = this.options.fontColor || "black"; + ctx.textAlign = "center"; // check for label alignment - if (this.options.labelAlignment != 'horizontal') { + if (this.options.labelAlignment != "horizontal") { var lineMargin = 2; - if (this.options.labelAlignment == 'line-above') { + if (this.options.labelAlignment == "line-above") { ctx.textBaseline = "alphabetic"; yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } - else if (this.options.labelAlignment == 'line-below') { + } else if (this.options.labelAlignment == "line-below") { ctx.textBaseline = "hanging"; - yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers - } - else { + yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + } else { ctx.textBaseline = "middle"; } - } - else { + } else { ctx.textBaseline = "middle"; } // check for strokeWidth - if (this.options.fontStrokeWidth > 0){ - ctx.lineWidth = this.options.fontStrokeWidth; + if (this.options.fontStrokeWidth > 0) { + ctx.lineWidth = this.options.fontStrokeWidth; ctx.strokeStyle = this.options.fontStrokeColor; - ctx.lineJoin = 'round'; + ctx.lineJoin = "round"; } - for (var i = 0; i < lineCount; i++) { - if(this.options.fontStrokeWidth > 0){ + for (var i = 0; i < lineCount; i++) { + if (this.options.fontStrokeWidth > 0) { ctx.strokeText(lines[i], x, yLine); } - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } }; /** @@ -29013,7 +27061,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {CanvasRenderingContext2D} ctx * @private */ - Edge.prototype._drawDashLine = function(ctx) { + Edge.prototype._drawDashLine = function (ctx) { // set style ctx.strokeStyle = this._getColor(ctx); ctx.lineWidth = this._getLineWidth(); @@ -29025,10 +27073,9 @@ return /******/ (function(modules) { // webpackBootstrap // configure the dash pattern var pattern = [0]; if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) { - pattern = [this.options.dash.length,this.options.dash.gap]; - } - else { - pattern = [5,5]; + pattern = [this.options.dash.length, this.options.dash.gap]; + } else { + pattern = [5, 5]; } // set dash settings for chrome or firefox @@ -29042,26 +27089,22 @@ return /******/ (function(modules) { // webpackBootstrap ctx.setLineDash([0]); ctx.lineDashOffset = 0; ctx.restore(); - } - else { // unsupporting smooth lines + } else { + // unsupporting smooth lines // draw dashed line ctx.beginPath(); - ctx.lineCap = 'round'; + ctx.lineCap = "round"; if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]); - } - else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.options.dash.length,this.options.dash.gap]); - } - else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } + { + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap, this.options.dash.altLength, this.options.dash.gap]); + } else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap]); + } else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } ctx.stroke(); } @@ -29069,11 +27112,10 @@ return /******/ (function(modules) { // webpackBootstrap if (this.label) { var point; if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { point = this._pointOnLine(0.5); } this._label(ctx, this.label, point.x, point.y); @@ -29090,7 +27132,7 @@ return /******/ (function(modules) { // webpackBootstrap return { x: (1 - percentage) * this.from.x + percentage * this.to.x, y: (1 - percentage) * this.from.y + percentage * this.to.y - } + }; }; /** @@ -29103,11 +27145,11 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { - var angle = (percentage - 3/8) * 2 * Math.PI; + var angle = (percentage - 3 / 8) * 2 * Math.PI; return { x: x + radius * Math.cos(angle), y: y - radius * Math.sin(angle) - } + }; }; /** @@ -29117,7 +27159,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {CanvasRenderingContext2D} ctx * @private */ - Edge.prototype._drawArrowCenter = function(ctx) { + Edge.prototype._drawArrowCenter = function (ctx) { var point; // set style ctx.strokeStyle = this._getColor(ctx); @@ -29128,15 +27170,14 @@ return /******/ (function(modules) { // webpackBootstrap // draw line var via = this._line(ctx); - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; // draw an arrow halfway the line if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { point = this._pointOnLine(0.5); } @@ -29148,11 +27189,10 @@ return /******/ (function(modules) { // webpackBootstrap if (this.label) { this._label(ctx, this.label, point.x, point.y); } - } - else { + } else { // draw circle var x, y; - var radius = 0.25 * Math.max(100,this.physics.springLength); + var radius = 0.25 * Math.max(100, this.physics.springLength); var node = this.from; if (!node.width) { node.resize(ctx); @@ -29160,8 +27200,7 @@ return /******/ (function(modules) { // webpackBootstrap if (node.width > node.height) { x = node.x + node.width * 0.5; y = node.y - radius; - } - else { + } else { x = node.x + radius; y = node.y - node.height * 0.5; } @@ -29183,14 +27222,14 @@ return /******/ (function(modules) { // webpackBootstrap } }; - Edge.prototype._pointOnBezier = function(t) { + Edge.prototype._pointOnBezier = function (t) { var via = this._getViaCoordinates(); - var x = Math.pow(1-t,2)*this.from.x + (2*t*(1 - t))*via.x + Math.pow(t,2)*this.to.x; - var y = Math.pow(1-t,2)*this.from.y + (2*t*(1 - t))*via.y + Math.pow(t,2)*this.to.y; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; - return {x:x,y:y}; - } + return { x: x, y: y }; + }; /** * This function uses binary search to look for the point where the bezier curve crosses the border of the node. @@ -29200,12 +27239,12 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {*} * @private */ - Edge.prototype._findBorderPosition = function(from,ctx) { + Edge.prototype._findBorderPosition = function (from, ctx) { var maxIterations = 10; var iteration = 0; var low = 0; var high = 1; - var pos,angle,distanceToBorder, distanceToNodes, difference; + var pos, angle, distanceToBorder, distanceToNodes, difference; var threshold = 0.2; var node = this.to; if (from == true) { @@ -29216,26 +27255,23 @@ return /******/ (function(modules) { // webpackBootstrap var middle = (low + high) * 0.5; pos = this._pointOnBezier(middle); - angle = Math.atan2((node.y - pos.y), (node.x - pos.x)); - distanceToBorder = node.distanceToBorder(ctx,angle); - distanceToNodes = Math.sqrt(Math.pow(pos.x-node.x,2) + Math.pow(pos.y-node.y,2)); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToNodes = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); difference = distanceToBorder - distanceToNodes; if (Math.abs(difference) < threshold) { break; // found - } - else if (difference < 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + } else if (difference < 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. if (from == false) { low = middle; - } - else { - high = middle; - } - } - else { - if (from == false) { + } else { high = middle; } - else { + } else { + if (from == false) { + high = middle; + } else { low = middle; } } @@ -29254,7 +27290,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {CanvasRenderingContext2D} ctx * @private */ - Edge.prototype._drawArrow = function(ctx) { + Edge.prototype._drawArrow = function (ctx) { // set style ctx.strokeStyle = this._getColor(ctx); ctx.fillStyle = ctx.strokeStyle; @@ -29272,13 +27308,12 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.smoothCurves.enabled == true) { var via = this._getViaCoordinates(); arrowPos = this._findBorderPosition(false, ctx); - var guidePos = this._pointOnBezier(Math.max(0.0, arrowPos.t - 0.1)) - angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x)); - } - else { - angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); + var guidePos = this._pointOnBezier(Math.max(0, arrowPos.t - 0.1)); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var dx = this.to.x - this.from.x; + var dy = this.to.y - this.from.y; var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); var toBorderDist = this.to.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; @@ -29290,7 +27325,7 @@ return /******/ (function(modules) { // webpackBootstrap // draw arrow at the end of the line length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrowPos.x,arrowPos.y, angle, length); + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); ctx.fill(); ctx.stroke(); @@ -29299,18 +27334,16 @@ return /******/ (function(modules) { // webpackBootstrap var point; if (this.options.smoothCurves.enabled == true && via != null) { point = this._pointOnBezier(0.5); - } - else { + } else { point = this._pointOnLine(0.5); } this._label(ctx, this.label, point.x, point.y); } - } - else { + } else { // draw circle var node = this.from; var x, y, arrow; - var radius = 0.25 * Math.max(100,this.physics.springLength); + var radius = 0.25 * Math.max(100, this.physics.springLength); if (!node.width) { node.resize(ctx); } @@ -29322,8 +27355,7 @@ return /******/ (function(modules) { // webpackBootstrap y: node.y, angle: 0.9 * Math.PI }; - } - else { + } else { x = node.x + radius; y = node.y - node.height * 0.5; arrow = { @@ -29363,7 +27395,8 @@ return /******/ (function(modules) { // webpackBootstrap * @param {number} y3 * @private */ - Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + Edge.prototype._getDistanceToEdge = function (x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point var returnValue = 0; if (this.from != this.to) { if (this.options.smoothCurves.enabled == true) { @@ -29371,76 +27404,67 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { xVia = this.via.x; yVia = this.via.y; - } - else { + } else { var via = this._getViaCoordinates(); xVia = via.x; yVia = via.y; } - var minDistance = 1e9; + var minDistance = 1000000000; var distance; - var i,t,x,y, lastX, lastY; + var i, t, x, y, lastX, lastY; for (i = 0; i < 10; i++) { - t = 0.1*i; - x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; - y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; + t = 0.1 * i; + x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; + y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; if (i > 0) { - distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3); + distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); minDistance = distance < minDistance ? distance : minDistance; } - lastX = x; lastY = y; + lastX = x;lastY = y; } returnValue = minDistance; + } else { + returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3); } - else { - returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3); - } - } - else { + } else { var x, y, dx, dy; var radius = 0.25 * this.physics.springLength; var node = this.from; if (node.width > node.height) { x = node.x + 0.5 * node.width; y = node.y - radius; - } - else { + } else { x = node.x + radius; y = node.y - 0.5 * node.height; } dx = x - x3; dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius); + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); } - if (this.labelDimensions.left < x3 && - this.labelDimensions.left + this.labelDimensions.width > x3 && - this.labelDimensions.top < y3 && - this.labelDimensions.top + this.labelDimensions.height > y3) { + if (this.labelDimensions.left < x3 && this.labelDimensions.left + this.labelDimensions.width > x3 && this.labelDimensions.top < y3 && this.labelDimensions.top + this.labelDimensions.height > y3) { return 0; - } - else { + } else { return returnValue; } }; - Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) { - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; + Edge.prototype._getDistanceToLine = function (x1, y1, x2, y2, x3, y3) { + var px = x2 - x1, + py = y2 - y1, + something = px * px + py * py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; if (u > 1) { u = 1; - } - else if (u < 0) { + } else if (u < 0) { u = 0; } var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + y = y1 + u * py, + dx = x - x3, + dy = y - y3; //# Note: If the actual distance does not matter, //# if you only want to compare what this function @@ -29448,7 +27472,7 @@ return /******/ (function(modules) { // webpackBootstrap //# can just return the squared distance instead //# (i.e. remove the sqrt) to gain a little performance - return Math.sqrt(dx*dx + dy*dy); + return Math.sqrt(dx * dx + dy * dy); }; /** @@ -29456,25 +27480,24 @@ return /******/ (function(modules) { // webpackBootstrap * * @param scale */ - Edge.prototype.setScale = function(scale) { - this.networkScaleInv = 1.0/scale; + Edge.prototype.setScale = function (scale) { + this.networkScaleInv = 1 / scale; }; - Edge.prototype.select = function() { + Edge.prototype.select = function () { this.selected = true; }; - Edge.prototype.unselect = function() { + Edge.prototype.unselect = function () { this.selected = false; }; - Edge.prototype.positionBezierNode = function() { + Edge.prototype.positionBezierNode = function () { if (this.via !== null && this.from !== null && this.to !== null) { this.via.x = 0.5 * (this.from.x + this.to.x); this.via.y = 0.5 * (this.from.y + this.to.y); - } - else if (this.via !== null) { + } else if (this.via !== null) { this.via.x = 0; this.via.y = 0; } @@ -29485,26 +27508,24 @@ return /******/ (function(modules) { // webpackBootstrap * In order to enable this, only set the this.controlNodesEnabled to true. * @param ctx */ - Edge.prototype._drawControlNodes = function(ctx) { + Edge.prototype._drawControlNodes = function (ctx) { if (this.controlNodesEnabled == true) { if (this.controlNodes.from === null && this.controlNodes.to === null) { var nodeIdFrom = "edgeIdFrom:".concat(this.id); var nodeIdTo = "edgeIdTo:".concat(this.id); var constants = { - nodes:{group:'', radius:7, borderWidth:2, borderWidthSelected: 2}, - physics:{damping:0}, - clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}} - }; - this.controlNodes.from = new Node( - {id:nodeIdFrom, - shape:'dot', - color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}} - },{},{},constants); - this.controlNodes.to = new Node( - {id:nodeIdTo, - shape:'dot', - color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}} - },{},{},constants); + nodes: { group: "", radius: 7, borderWidth: 2, borderWidthSelected: 2 }, + physics: { damping: 0 }, + clustering: { maxNodeSizeIncrements: 0, nodeScaling: { width: 0, height: 0, radius: 0 } } + }; + this.controlNodes.from = new Node({ id: nodeIdFrom, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } + }, {}, {}, constants); + this.controlNodes.to = new Node({ id: nodeIdTo, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } + }, {}, {}, constants); } this.controlNodes.positions = {}; @@ -29521,9 +27542,8 @@ return /******/ (function(modules) { // webpackBootstrap this.controlNodes.from.draw(ctx); this.controlNodes.to.draw(ctx); - } - else { - this.controlNodes = {from:null, to:null, positions:{}}; + } else { + this.controlNodes = { from: null, to: null, positions: {} }; } }; @@ -29531,7 +27551,7 @@ return /******/ (function(modules) { // webpackBootstrap * Enable control nodes. * @private */ - Edge.prototype._enableControlNodes = function() { + Edge.prototype._enableControlNodes = function () { this.fromBackup = this.from; this.toBackup = this.to; this.controlNodesEnabled = true; @@ -29541,13 +27561,14 @@ return /******/ (function(modules) { // webpackBootstrap * disable control nodes and remove from dynamicEdges from old node * @private */ - Edge.prototype._disableControlNodes = function() { + Edge.prototype._disableControlNodes = function () { this.fromId = this.from.id; this.toId = this.to.id; - if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges + if (this.fromId != this.fromBackup.id) { + // from was changed, remove edge from old 'from' node dynamic edges this.fromBackup.detachEdge(this); - } - else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges + } else if (this.toId != this.toBackup.id) { + // to was changed, remove edge from old 'to' node dynamic edges this.toBackup.detachEdge(this); } @@ -29564,22 +27585,20 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {null} * @private */ - Edge.prototype._getSelectedControlNode = function(x,y) { + Edge.prototype._getSelectedControlNode = function (x, y) { var positions = this.controlNodes.positions; - var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2)); - var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2)); + var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2)); + var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2)); if (fromDistance < 15) { this.connectedNode = this.from; this.from = this.controlNodes.from; return this.controlNodes.from; - } - else if (toDistance < 15) { + } else if (toDistance < 15) { this.connectedNode = this.to; this.to = this.controlNodes.to; return this.controlNodes.to; - } - else { + } else { return null; } }; @@ -29589,13 +27608,12 @@ return /******/ (function(modules) { // webpackBootstrap * this resets the control nodes to their original position. * @private */ - Edge.prototype._restoreControlNodes = function() { + Edge.prototype._restoreControlNodes = function () { if (this.controlNodes.from.selected == true) { this.from = this.connectedNode; this.connectedNode = null; this.controlNodes.from.unselect(); - } - else if (this.controlNodes.to.selected == true) { + } else if (this.controlNodes.to.selected == true) { this.to = this.connectedNode; this.connectedNode = null; this.controlNodes.to.unselect(); @@ -29608,23 +27626,22 @@ return /******/ (function(modules) { // webpackBootstrap * @param ctx * @returns {x: *, y: *} */ - Edge.prototype.getControlNodeFromPosition = function(ctx) { + Edge.prototype.getControlNodeFromPosition = function (ctx) { // draw arrow head var controlnodeFromPos; if (this.options.smoothCurves.enabled == true) { controlnodeFromPos = this._findBorderPosition(true, ctx); - } - else { - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); + } else { + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var dx = this.to.x - this.from.x; + var dy = this.to.y - this.from.y; var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; controlnodeFromPos = {}; - controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; - controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + controlnodeFromPos.x = fromBorderPoint * this.from.x + (1 - fromBorderPoint) * this.to.x; + controlnodeFromPos.y = fromBorderPoint * this.from.y + (1 - fromBorderPoint) * this.to.y; } return controlnodeFromPos; @@ -29636,16 +27653,15 @@ return /******/ (function(modules) { // webpackBootstrap * @param ctx * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} */ - Edge.prototype.getControlNodeToPosition = function(ctx) { + Edge.prototype.getControlNodeToPosition = function (ctx) { // draw arrow head - var controlnodeFromPos,controlnodeToPos; + var controlnodeFromPos, controlnodeToPos; if (this.options.smoothCurves.enabled == true) { controlnodeToPos = this._findBorderPosition(false, ctx); - } - else { - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); + } else { + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var dx = this.to.x - this.from.x; + var dy = this.to.y - this.from.y; var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); var toBorderDist = this.to.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; @@ -29664,6 +27680,8 @@ return /******/ (function(modules) { // webpackBootstrap /* 58 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + /** * Popup is a class to create a popup window with some text * @param {Element} container The container object. @@ -29676,8 +27694,7 @@ return /******/ (function(modules) { // webpackBootstrap function Popup(container, x, y, text, style) { if (container) { this.container = container; - } - else { + } else { this.container = document.body; } @@ -29692,14 +27709,14 @@ return /******/ (function(modules) { // webpackBootstrap } else { // for backwards compatibility, in case clients other than Network are creating Popup directly style = { - fontColor: 'black', + fontColor: "black", fontSize: 14, // px - fontFace: 'verdana', + fontFace: "verdana", color: { - border: '#666', - background: '#FFFFC6' + border: "#666", + background: "#FFFFC6" } - } + }; } } @@ -29716,13 +27733,13 @@ return /******/ (function(modules) { // webpackBootstrap } // create the frame - this.frame = document.createElement('div'); - this.frame.className = 'network-tooltip'; - this.frame.style.color = style.fontColor; + this.frame = document.createElement("div"); + this.frame.className = "network-tooltip"; + this.frame.style.color = style.fontColor; this.frame.style.backgroundColor = style.color.background; - this.frame.style.borderColor = style.color.border; - this.frame.style.fontSize = style.fontSize + 'px'; - this.frame.style.fontFamily = style.fontFace; + this.frame.style.borderColor = style.color.border; + this.frame.style.fontSize = style.fontSize + "px"; + this.frame.style.fontFamily = style.fontFace; this.container.appendChild(this.frame); } @@ -29730,7 +27747,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {number} x Horizontal position of the popup window * @param {number} y Vertical position of the popup window */ - Popup.prototype.setPosition = function(x, y) { + Popup.prototype.setPosition = function (x, y) { this.x = parseInt(x); this.y = parseInt(y); }; @@ -29739,12 +27756,11 @@ return /******/ (function(modules) { // webpackBootstrap * Set the content for the popup window. This can be HTML code or text. * @param {string | Element} content */ - Popup.prototype.setText = function(content) { + Popup.prototype.setText = function (content) { if (content instanceof Element) { - this.frame.innerHTML = ''; + this.frame.innerHTML = ""; this.frame.appendChild(content); - } - else { + } else { this.frame.innerHTML = content; // string containing text or HTML } }; @@ -29760,11 +27776,11 @@ return /******/ (function(modules) { // webpackBootstrap if (show) { var height = this.frame.clientHeight; - var width = this.frame.clientWidth; + var width = this.frame.clientWidth; var maxHeight = this.frame.parentNode.clientHeight; var maxWidth = this.frame.parentNode.clientWidth; - var top = (this.y - height); + var top = this.y - height; if (top + height + this.padding > maxHeight) { top = maxHeight - height - this.padding; } @@ -29784,8 +27800,7 @@ return /******/ (function(modules) { // webpackBootstrap this.frame.style.top = top + "px"; this.frame.style.visibility = "visible"; this.hidden = false; - } - else { + } else { this.hide(); } }; @@ -29800,18 +27815,16 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Popup; - /***/ }, /* 59 */ /***/ function(module, exports, __webpack_require__) { - var PhysicsMixin = __webpack_require__(60); - var ClusterMixin = __webpack_require__(64); - var SectorsMixin = __webpack_require__(65); - var SelectionMixin = __webpack_require__(66); - var ManipulationMixin = __webpack_require__(67); - var NavigationMixin = __webpack_require__(68); - var HierarchicalLayoutMixin = __webpack_require__(69); + "use strict"; + + var SelectionMixin = __webpack_require__(60); + var ManipulationMixin = __webpack_require__(61); + var NavigationMixin = __webpack_require__(62); + var HierarchicalLayoutMixin = __webpack_require__(63); /** * Load a mixin into the network object @@ -29853,50 +27866,12 @@ return /******/ (function(modules) { // webpackBootstrap this._loadSelectedForceSolver(); if (this.constants.configurePhysics == true) { this._loadPhysicsConfiguration(); - } - else { + } else { this._cleanupPhysicsConfiguration(); } }; - /** - * Mixin the cluster system and initialize the parameters required. - * - * @private - */ - exports._loadClusterSystem = function () { - this.clusterSession = 0; - this.hubThreshold = 5; - this._loadMixin(ClusterMixin); - }; - - - /** - * Mixin the sector system and initialize the parameters required - * - * @private - */ - exports._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["support"] = {"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 - - this._loadMixin(SectorsMixin); - }; /** @@ -29905,7 +27880,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ exports._loadSelectionSystem = function () { - this.selectionObj = {nodes: {}, edges: {}}; + this.selectionObj = { nodes: {}, edges: {} }; this._loadMixin(SelectionMixin); }; @@ -29924,32 +27899,30 @@ return /******/ (function(modules) { // webpackBootstrap if (this.constants.dataManipulation.enabled == true) { // load the manipulator HTML elements. All styling done in css. if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement('div'); - this.manipulationDiv.className = 'network-manipulationDiv'; + this.manipulationDiv = document.createElement("div"); + this.manipulationDiv.className = "network-manipulationDiv"; if (this.editMode == true) { this.manipulationDiv.style.display = "block"; - } - else { + } else { this.manipulationDiv.style.display = "none"; } this.frame.appendChild(this.manipulationDiv); } if (this.editModeDiv === undefined) { - this.editModeDiv = document.createElement('div'); - this.editModeDiv.className = 'network-manipulation-editMode'; + this.editModeDiv = document.createElement("div"); + this.editModeDiv.className = "network-manipulation-editMode"; if (this.editMode == true) { this.editModeDiv.style.display = "none"; - } - else { + } else { this.editModeDiv.style.display = "block"; } this.frame.appendChild(this.editModeDiv); } if (this.closeDiv === undefined) { - this.closeDiv = document.createElement('div'); - this.closeDiv.className = 'network-manipulation-closeDiv'; + this.closeDiv = document.createElement("div"); + this.closeDiv.className = "network-manipulation-closeDiv"; this.closeDiv.style.display = this.manipulationDiv.style.display; this.frame.appendChild(this.closeDiv); } @@ -29959,8 +27932,7 @@ return /******/ (function(modules) { // webpackBootstrap // create the manipulator toolbar this._createManipulatorBar(); - } - else { + } else { if (this.manipulationDiv !== undefined) { // removes all the bindings and overloads this._createManipulatorBar(); @@ -30004,5357 +27976,6828 @@ return /******/ (function(modules) { // webpackBootstrap this._loadMixin(HierarchicalLayoutMixin); }; - /***/ }, /* 60 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); - var RepulsionMixin = __webpack_require__(61); - var HierarchialRepulsionMixin = __webpack_require__(62); - var BarnesHutMixin = __webpack_require__(63); + "use strict"; + + var Node = __webpack_require__(56); /** - * Toggling barnes Hut calculation on and off. * + * @param object + * @param overlappingNodes * @private */ - exports._toggleBarnesHut = function () { - this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; - this._loadSelectedForceSolver(); - this.moving = true; - this.start(); + exports._getNodesOverlappingWith = function (object, overlappingNodes) { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } }; - /** - * This loads the node force solver based on the barnes hut or repulsion algorithm - * + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - exports._loadSelectedForceSolver = function () { - // this overloads the this._calculateNodeForces - if (this.constants.physics.barnesHut.enabled == true) { - this._clearMixin(RepulsionMixin); - this._clearMixin(HierarchialRepulsionMixin); - - this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; - this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; - this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; - this.constants.physics.damping = this.constants.physics.barnesHut.damping; - - this._loadMixin(BarnesHutMixin); - } - else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._clearMixin(BarnesHutMixin); - this._clearMixin(RepulsionMixin); - - this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; - this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; - this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; - this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; - - this._loadMixin(HierarchialRepulsionMixin); - } - else { - this._clearMixin(BarnesHutMixin); - this._clearMixin(HierarchialRepulsionMixin); - this.barnesHutTree = undefined; - - this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; - this.constants.physics.springLength = this.constants.physics.repulsion.springLength; - this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; - this.constants.physics.damping = this.constants.physics.repulsion.damping; - - this._loadMixin(RepulsionMixin); - } + exports._getAllNodesOverlappingWith = function (object) { + var overlappingNodes = []; + this._getNodesOverlappingWith(object, overlappingNodes); + return overlappingNodes; }; + /** - * 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. + * Return a position object in canvasspace from a single point in screenspace * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ - exports._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); - } + exports._pointerToPositionObject = function (pointer) { + var x = this._XconvertDOMtoCanvas(pointer.x); + var y = this._YconvertDOMtoCanvas(pointer.y); - // we now start the force calculation - this._calculateForces(); - } + return { + left: x, + top: y, + right: x, + bottom: y + }; }; /** - * Calculate the external forces acting on the nodes - * Forces are caused by: edges, repulsing forces between nodes, gravity + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node * @private */ - exports._calculateForces = function () { - // 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 - - this._calculateGravitationalForces(); - this._calculateNodeForces(); + exports._getNodeAt = function (pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - if (this.constants.physics.springConstant > 0) { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._calculateSpringForcesWithSupport(); - } - else { - if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._calculateHierarchicalSpringForces(); - } - else { - this._calculateSpringForces(); - } - } + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } else { + return null; } }; /** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.nodes with the support nodes. - * + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - exports._updateCalculationNodes = function () { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this.calculationNodes = {}; - this.calculationNodeIndices = []; - - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId] = this.nodes[nodeId]; - } - } - var supportNodes = this.sectors['support']['nodes']; - for (var supportNodeId in supportNodes) { - if (supportNodes.hasOwnProperty(supportNodeId)) { - if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { - this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; - } - else { - supportNodes[supportNodeId]._setForce(0, 0); - } - } - } - - for (var idx in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(idx)) { - this.calculationNodeIndices.push(idx); + exports._getEdgesOverlappingWith = function (object, overlappingEdges) { + var edges = this.body.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); } } } - else { - this.calculationNodes = this.nodes; - this.calculationNodeIndices = this.nodeIndices; - } }; /** - * this function applies the central gravity effect to keep groups from floating off - * + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - exports._calculateGravitationalForces = function () { - var dx, dy, distance, node, i; - var nodes = this.calculationNodes; - var gravity = this.constants.physics.centralGravity; - var gravityForce = 0; - - for (i = 0; i < this.calculationNodeIndices.length; i++) { - node = nodes[this.calculationNodeIndices[i]]; - node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default" && gravity != 0) { - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - gravityForce = (distance == 0) ? 0 : (gravity / distance); - node.fx = dx * gravityForce; - node.fy = dy * gravityForce; - } - else { - node.fx = 0; - node.fy = 0; - } - } + exports._getAllEdgesOverlappingWith = function (object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; }; - - - /** - * this function calculates the effects of the springs in the case of unsmooth curves. + * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call + * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * + * @param pointer + * @returns {null} * @private */ - exports._calculateSpringForces = function () { - var edgeLength, edge, edgeId; - var dx, dy, fx, fy, springForce, distance; - var edges = this.edges; - - // 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)) { - edgeLength = edge.physics.springLength; - // this implies that the edges between big clusters are longer - edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; - - dx = (edge.from.x - edge.to.x); - dy = (edge.from.y - edge.to.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; + exports._getEdgeAt = function (pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - edge.from.fx += fx; - edge.from.fy += fy; - edge.to.fx -= fx; - edge.to.fy -= fy; - } - } - } + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return null; } }; - - /** - * This function calculates the springforces on the nodes, accounting for the support nodes. + * Add object to the selection array. * + * @param obj * @private */ - exports._calculateSpringForcesWithSupport = function () { - var edgeLength, edge, edgeId, combinedClusterSize; - var edges = this.edges; - - // 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)) { - if (edge.via != null) { - var node1 = edge.to; - var node2 = edge.via; - var node3 = edge.from; - - edgeLength = edge.physics.springLength; - - combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; - - // this implies that the edges between big clusters are longer - edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } - } - } - } + exports._addToSelection = function (obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; } }; - /** - * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. + * Add object to the selection array. * - * @param node1 - * @param node2 - * @param edgeLength + * @param obj * @private */ - exports._calculateSpringForce = function (node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = (node1.x - node2.x); - dy = (node1.y - node2.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; + exports._addToHover = function (obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - node1.fx += fx; - node1.fy += fy; - node2.fx -= fx; - node2.fy -= fy; }; - exports._cleanupPhysicsConfiguration = function() { - if (this.physicsConfiguration !== undefined) { - while (this.physicsConfiguration.hasChildNodes()) { - this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild); - } - - this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration); - this.physicsConfiguration = undefined; + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + exports._removeFromSelection = function (obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } else { + delete this.selectionObj.edges[obj.id]; } - } + }; /** - * Load the HTML for the physics config and bind it + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._loadPhysicsConfiguration = function () { - if (this.physicsConfiguration === undefined) { - this.backupConstants = {}; - util.deepExtend(this.backupConstants,this.constants); - - var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10); - var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10) - - var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; - this.physicsConfiguration = document.createElement('div'); - this.physicsConfiguration.className = "PhysicsConfiguration"; - this.physicsConfiguration.innerHTML = '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Options:
' - this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); - this.optionsDiv = document.createElement("div"); - this.optionsDiv.style.fontSize = "14px"; - this.optionsDiv.style.fontFamily = "verdana"; - this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); - - var rangeElement; - rangeElement = document.getElementById('graph_BH_gc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); - rangeElement = document.getElementById('graph_BH_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_BH_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_BH_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_BH_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_R_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); - rangeElement = document.getElementById('graph_R_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_R_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_R_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_R_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_H_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - rangeElement = document.getElementById('graph_H_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_H_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_H_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_H_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); - rangeElement = document.getElementById('graph_H_direction'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); - rangeElement = document.getElementById('graph_H_levsep'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); - rangeElement = document.getElementById('graph_H_nspac'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); - - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - var radioButton3 = document.getElementById("graph_physicsMethod3"); - radioButton2.checked = true; - if (this.constants.physics.barnesHut.enabled) { - radioButton1.checked = true; - } - if (this.constants.hierarchicalLayout.enabled) { - radioButton3.checked = true; - } - - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - var graph_repositionNodes = document.getElementById("graph_repositionNodes"); - var graph_generateOptions = document.getElementById("graph_generateOptions"); - - graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); - graph_repositionNodes.onclick = graphRepositionNodes.bind(this); - graph_generateOptions.onclick = graphGenerateOptions.bind(this); - if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { - graph_toggleSmooth.style.background = "#A4FF56"; + exports._unselectAll = function (doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); } - else { - graph_toggleSmooth.style.background = "#FF8532"; + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); } + } + this.selectionObj = { nodes: {}, edges: {} }; - switchConfigurations.apply(this); - - radioButton1.onchange = switchConfigurations.bind(this); - radioButton2.onchange = switchConfigurations.bind(this); - radioButton3.onchange = switchConfigurations.bind(this); + if (doNotTrigger == false) { + this.emit("select", this.getSelection()); } }; /** - * This overwrites the this.constants. + * Unselect all clusters. The selectionObj is useful for this. * - * @param constantsVariableName - * @param value + * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._overWriteGraphConstants = function (constantsVariableName, value) { - var nameArray = constantsVariableName.split("_"); - if (nameArray.length == 1) { - this.constants[nameArray[0]] = value; + exports._unselectClusters = function (doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; } - else if (nameArray.length == 2) { - this.constants[nameArray[0]][nameArray[1]] = value; + + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + this.selectionObj.nodes[nodeId].unselect(); + this._removeFromSelection(this.selectionObj.nodes[nodeId]); + } + } } - else if (nameArray.length == 3) { - this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; + + if (doNotTrigger == false) { + this.emit("select", this.getSelection()); } }; /** - * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. + * return the number of selected nodes + * + * @returns {number} + * @private */ - function graphToggleSmoothCurves () { - this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} - - this._configureSmoothCurves(false); - } + exports._getSelectedNodeCount = function () { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; + }; /** - * this function is used to scramble the nodes + * return the selected node * + * @returns {number} + * @private */ - function graphRepositionNodes () { - for (var nodeId in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; - this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; + exports._getSelectedNode = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; } } - if (this.constants.hierarchicalLayout.enabled == true) { - this._setupHierarchicalLayout(); - showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); - showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); - showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); - showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); - } - else { - this.repositionNodes(); - } - this.moving = true; - this.start(); - } + return null; + }; /** - * this is used to generate an options file from the playing with physics system. + * return the selected edge + * + * @returns {number} + * @private */ - function graphGenerateOptions () { - var options = "No options are required, default values used."; - var optionsSpecific = []; - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - if (radioButton1.checked == true) { - if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options = "var options = {"; - options += "physics: {barnesHut: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' - } - if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { - if (optionsSpecific.length == 0) {options = "var options = {";} - else {options += ", "} - options += "smoothCurves: " + this.constants.smoothCurves.enabled; - } - if (options != "No options are required, default values used.") { - options += '};' - } - } - else if (radioButton2.checked == true) { - options = "var options = {"; - options += "physics: {barnesHut: {enabled: false}"; - if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += ", repulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' - } - if (optionsSpecific.length == 0) {options += "}"} - if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { - options += ", smoothCurves: " + this.constants.smoothCurves; - } - options += '};' - } - else { - options = "var options = {"; - if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += "physics: {hierarchicalRepulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", "; - } - } - options += '}},'; - } - options += 'hierarchicalLayout: {'; - optionsSpecific = []; - if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} - if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} - if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} - if (optionsSpecific.length != 0) { - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}' - } - else { - options += "enabled:true}"; + exports._getSelectedEdge = function () { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; } - options += '};' } + return null; + }; - this.optionsDiv.innerHTML = options; - } - /** - * this is used to switch between barnesHut, repulsion and hierarchical. + * return the number of selected edges * + * @returns {number} + * @private */ - function switchConfigurations () { - var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; - var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; - var tableId = "graph_" + radioButton + "_table"; - var table = document.getElementById(tableId); - table.style.display = "block"; - for (var i = 0; i < ids.length; i++) { - if (ids[i] != tableId) { - table = document.getElementById(ids[i]); - table.style.display = "none"; - } - } - this._restoreNodes(); - if (radioButton == "R") { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = false; - } - else if (radioButton == "H") { - if (this.constants.hierarchicalLayout.enabled == false) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - this.constants.smoothCurves.enabled = false; - this._setupHierarchicalLayout(); + exports._getSelectedEdgeCount = function () { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; } } - else { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = true; - } - this._loadSelectedForceSolver(); - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} - this.moving = true; - this.start(); - } + return count; + }; /** - * this generates the ranges depending on the iniital values. + * return the number of selected objects. * - * @param id - * @param map - * @param constantsVariableName + * @returns {number} + * @private */ - function showValueOfRange (id,map,constantsVariableName) { - var valueId = id + "_value"; - var rangeValue = document.getElementById(id).value; - - if (Array.isArray(map)) { - document.getElementById(valueId).value = map[parseInt(rangeValue)]; - this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); - } - else { - document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); - this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); + exports._getSelectedObjectCount = function () { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } } - - if (constantsVariableName == "hierarchicalLayout_direction" || - constantsVariableName == "hierarchicalLayout_levelSeparation" || - constantsVariableName == "hierarchicalLayout_nodeSpacing") { - this._setupHierarchicalLayout(); + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } } - this.moving = true; - this.start(); - } - - - - -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { + return count; + }; /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. + * Check if anything is selected * + * @returns {boolean} * @private */ - exports._calculateNodeForces = function () { - var dx, dy, angle, distance, fx, fy, combinedClusterSize, - repulsingForce, node1, node2, i, j; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - // approximation constants - var a_base = -2 / 3; - var b = 4 / 3; - - // repulsing forces between nodes - var nodeDistance = this.constants.physics.repulsion.nodeDistance; - var minimumDistance = nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // same condition as BarnesHut, making sure nodes are never 100% overlapping. - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; - } - - minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); - var a = a_base / minimumDistance; - if (distance < 2 * minimumDistance) { - if (distance < 0.5 * minimumDistance) { - repulsingForce = 1.0; - } - else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) - } - - // amplify the repulsion for clusters. - repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; - repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance); - - fx = dx * repulsingForce; - fy = dy * repulsingForce; - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; - - } + exports._selectionIsEmpty = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; } } + return true; }; -/***/ }, -/* 62 */ -/***/ function(module, exports, __webpack_require__) { - /** - * Calculate the forces the nodes apply on eachother based on a repulsion field. - * This field is linearly approximated. + * check if one of the selected nodes is a cluster. * + * @returns {boolean} * @private */ - exports._calculateNodeForces = function () { - var dx, dy, distance, fx, fy, - repulsingForce, node1, node2, i, j; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - // repulsing forces between nodes - var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - - // nodes only affect nodes on their level - if (node1.level == node2.level) { - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); - } - else { - repulsingForce = 0; - } - // normalize force with - if (distance == 0) { - distance = 0.01; - } - else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; - - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; + exports._clusterInSelection = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; } } } + return false; }; - /** - * this function calculates the effects of the springs in the case of unsmooth curves. + * select the edges connected to the node that is being selected * + * @param {Node} node * @private */ - exports._calculateHierarchicalSpringForces = function () { - var edgeLength, edge, edgeId; - var dx, dy, fx, fy, springForce, distance; - var edges = this.edges; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - - for (var i = 0; i < nodeIndices.length; i++) { - var node1 = nodes[nodeIndices[i]]; - node1.springFx = 0; - node1.springFy = 0; - } - - - // 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)) { - edgeLength = edge.physics.springLength; - // this implies that the edges between big clusters are longer - edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; - - dx = (edge.from.x - edge.to.x); - dy = (edge.from.y - edge.to.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - - - if (edge.to.level != edge.from.level) { - edge.to.springFx -= fx; - edge.to.springFy -= fy; - edge.from.springFx += fx; - edge.from.springFy += fy; - } - else { - var factor = 0.5; - edge.to.fx -= factor*fx; - edge.to.fy -= factor*fy; - edge.from.fx += factor*fx; - edge.from.fy += factor*fy; - } - } - } - } - } - - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); - springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); - - node.fx += springFx; - node.fy += springFy; - } - - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - totalFx += node.fx; - totalFy += node.fy; - } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; - - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - node.fx -= correctionFx; - node.fy -= correctionFy; + exports._selectConnectedEdges = function (node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); } - }; -/***/ }, -/* 63 */ -/***/ function(module, exports, __webpack_require__) { - /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. + * select the edges connected to the node that is being selected * + * @param {Node} node * @private */ - exports._calculateNodeForces = function() { - if (this.constants.physics.barnesHut.gravitationalConstant != 0) { - var node; - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - var nodeCount = nodeIndices.length; - - this._formBarnesHutTree(nodes,nodeIndices); - - var barnesHutTree = this.barnesHutTree; - - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHut condition - this._getForceContribution(barnesHutTree.root.children.NW,node); - this._getForceContribution(barnesHutTree.root.children.NE,node); - this._getForceContribution(barnesHutTree.root.children.SW,node); - this._getForceContribution(barnesHutTree.root.children.SE,node); - } - } + exports._hoverConnectedEdges = function (node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); } }; /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. + * unselect the edges connected to the node that is being selected * - * @param parentBranch - * @param node + * @param {Node} node * @private */ - exports._getForceContribution = function(parentBranch,node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx,dy,distance; - - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // BarnesHut condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; - } - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } - else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW,node); - this._getForceContribution(parentBranch.children.NE,node); - this._getForceContribution(parentBranch.children.SW,node); - this._getForceContribution(parentBranch.children.SE,node); - } - else { // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5*Math.random(); - dx = distance; - } - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } - } - } + exports._unselectConnectedEdges = function (node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); } }; + + + /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * 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 nodes - * @param nodeIndices + * @param {Node || Edge} object + * @param {Boolean} append + * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._formBarnesHutTree = function(nodes,nodeIndices) { - var node; - var nodeCount = nodeIndices.length; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX =-Number.MAX_VALUE, - maxY =-Number.MAX_VALUE; - - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { minX = x; } - if (x > maxX) { maxX = x; } - if (y < minY) { minY = y; } - if (y > maxY) { maxY = y; } - } - } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize - else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize - - - var minimumTreeSize = 1e-5; - var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); - - // construct the barnesHutTree - var barnesHutTree = { - root:{ - centerOfMass: {x:0, y:0}, - mass:0, - range: { - minX: centerX-halfRootSize,maxX:centerX+halfRootSize, - minY: centerY-halfRootSize,maxY:centerY+halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data:null}, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); + exports._selectObject = function (object, append, doNotTrigger, highlightEdges, overrideSelectable) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + if (highlightEdges === undefined) { + highlightEdges = true; + } + + if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { + this._unselectAll(true); + } - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root,node); + // selectable allows the object to be selected. Override can be used if needed to bypass this. + if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) { + object.select(); + this._addToSelection(object); + if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { + this._selectConnectedEdges(object); } } + // do not select the object if selectable is false, only add it to selection to allow drag to work + else if (object.selected == false) { + this._addToSelection(object); + doNotTrigger = true; + } else { + object.unselect(); + this._removeFromSelection(object); + } - // make global - this.barnesHutTree = barnesHutTree + if (doNotTrigger == false) { + this.emit("select", this.getSelection()); + } }; /** - * this updates the mass of a branch. this is increased by adding a node. + * 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 parentBranch - * @param node + * @param {Node || Edge} object * @private */ - exports._updateBranchMass = function(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1/totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; - - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); - parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; - + exports._blurObject = function (object) { + if (object.hover == true) { + object.hover = false; + this.emit("blurNode", { node: object.id }); + } }; - /** - * determine in which branch the node will be placed. + * 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 parentBranch - * @param node - * @param skipMassUpdate + * @param {Node || Edge} object * @private */ - exports._placeInTree = function(parentBranch,node,skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch,node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { // in NW - this._placeInRegion(parentBranch,node,"NW"); - } - else { // in SW - this._placeInRegion(parentBranch,node,"SW"); + exports._hoverObject = function (object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.emit("hoverNode", { node: object.id }); } } - else { // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { // in NE - this._placeInRegion(parentBranch,node,"NE"); - } - else { // in SE - this._placeInRegion(parentBranch,node,"SE"); - } + if (object instanceof Node) { + this._hoverConnectedEdges(object); } }; /** - * actually place the node in a region (or branch) + * 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 parentBranch - * @param node - * @param region + * @param {Object} pointer * @private */ - exports._placeInRegion = function(parentBranch,node,region) { - switch (parentBranch.children[region].childrenCount) { - case 0: // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region],node); - break; - case 1: // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && - parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } - else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region],node); - } - break; - case 4: // place in branch - this._placeInTree(parentBranch.children[region],node); - break; - } - }; + exports._handleTouch = function (pointer) {}; /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. + * handles the selection part of the tap; * - * @param parentBranch + * @param {Object} pointer * @private */ - exports._splitBranch = function(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch,"NW"); - this._insertRegion(parentBranch,"NE"); - this._insertRegion(parentBranch,"SW"); - this._insertRegion(parentBranch,"SE"); - - if (containedNode != null) { - this._placeInTree(parentBranch,containedNode); + exports._handleTap = function (pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node, false); + } else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge, false); + } else { + this._unselectAll(); + } } + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } + }; + this.emit("click", properties); + this._requestRedraw(); }; /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch + * handles the selection part of the double tap and opens a cluster if needed * - * @param parentBranch - * @param region - * @param parentRange + * @param {Object} pointer * @private */ - exports._insertRegion = function(parentBranch, region) { - var minX,maxX,minY,maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; + exports._handleDoubleTap = function (pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = { x: this._XconvertDOMtoCanvas(pointer.x), + y: this._YconvertDOMtoCanvas(pointer.y) }; + this.openCluster(node); } - - - parentBranch.children[region] = { - centerOfMass:{x:0,y:0}, - mass:0, - range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: {data:null}, - maxWidth: 0, - level: parentBranch.level+1, - childrenCount: 0 + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } }; + this.emit("doubleClick", properties); }; /** - * This function is for debugging purposed, it draws the tree. + * Handle the onHold selection part * - * @param ctx - * @param color + * @param pointer * @private */ - exports._drawTree = function(ctx,color) { - if (this.barnesHutTree !== undefined) { - - ctx.lineWidth = 1; - - this._drawBranch(this.barnesHutTree.root,ctx,color); + exports._handleOnHold = function (pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node, true); + } else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge, true); + } } + this._requestRedraw(); }; /** - * This function is for debugging purposes. It draws the branches recursively. + * handle the onRelease event. These functions are here for the navigation controls module + * and data manipulation module. * - * @param branch - * @param ctx - * @param color - * @private + * @private */ - exports._drawBranch = function(branch,ctx,color) { - if (color === undefined) { - color = "#FF0000"; - } - - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW,ctx); - this._drawBranch(branch.children.NE,ctx); - this._drawBranch(branch.children.SE,ctx); - this._drawBranch(branch.children.SW,ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.minY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.minY); - ctx.stroke(); - - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ + exports._handleOnRelease = function (pointer) { + this._manipulationReleaseOverload(pointer); + this._navigationReleaseOverload(pointer); }; - -/***/ }, -/* 64 */ -/***/ function(module, exports, __webpack_require__) { + exports._manipulationReleaseOverload = function (pointer) {}; + exports._navigationReleaseOverload = function (pointer) {}; /** - * Creation of the ClusterMixin var. * - * This contains all the functions the Network object can use to employ clustering + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection */ - - /** - * This is only called in the constructor of the network object - * - */ - exports.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.constants.stabilize == true) { - this._stabilize(); - } - this.start(); + exports.getSelection = function () { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; }; /** - * This function clusters until the initialMaxNodes has been reached * - * @param {Number} maxNumberOfNodes - * @param {Boolean} reposition + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. */ - exports.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.0) { - this.forceAggregateHubs(true); - this.normalizeClusterLevels(); - } - else { - this.increaseClusterLevel(); // this also includes a cluster normalization + exports.getSelectedNodes = function () { + var idArray = []; + if (this.constants.selectable == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } } - this.forceAggregateHubs(true); - numberOfNodes = this.nodeIndices.length; - level += 1; - } - - // after the clustering we reposition the nodes to reduce the initial chaos - if (level > 0 && reposition == true) { - this.repositionNodes(); } - this._updateCalculationNodes(); + return idArray; }; /** - * This function can be called to open up a specific cluster. - * It will unpack the cluster back one level. * - * @param node | Node object: cluster to open. + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. */ - exports.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 loads a new sector, loads the nodes and edges and nodeIndices of it. - this._addSector(node); - var level = 0; - - // we decluster until we reach a decent number of nodes - while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { - this.decreaseClusterLevel(); - level += 1; + exports.getSelectedEdges = function () { + var idArray = []; + if (this.constants.selectable == true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } } - - } - else { - this._expandClusterNode(node,false,true); - - // update the index list and labels - this._updateNodeIndexList(); - this._updateCalculationNodes(); - 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 - */ - exports.updateClustersDefault = function() { - if (this.constants.clustering.enabled == true && this.constants.clustering.clusterByZoom == true) { - this.updateClusters(0,false,false); } + return idArray; }; /** - * 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. + * select zero or more nodes DEPRICATED + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - exports.increaseClusterLevel = function() { - this.updateClusters(-1,false,true); + exports.setSelection = function () { + console.log("setSelection is deprecated. Please use selectNodes instead."); }; /** - * 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. + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] */ - exports.decreaseClusterLevel = function() { - this.updateClusters(1,false,true); - }; - + exports.selectNodes = function (selection, highlightEdges) { + var i, iMax, id; - /** - * 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 - * @param {Boolean} doNotStart | if true do not call start - * - */ - exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - var detectedZoomingIn = (this.previousScale < this.scale && zoomDirection == 0); - var detectedZoomingOut = (this.previousScale > this.scale && zoomDirection == 0); + // first unselect any selected node + this._unselectAll(true); - // on zoom out collapse the sector if the scale is at the level the sector was made - if (detectedZoomingOut == true) { - this._collapseSector(); - } + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - // check if we zoom in or out - if (detectedZoomingOut == true || 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 (detectedZoomingIn == true || 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._openClusters(recursive, false); + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); } + this._selectObject(node, true, true, highlightEdges, true); } - this._updateNodeIndexList(); + this.redraw(); + }; - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs - if (this.nodeIndices.length == amountOfNodes && (detectedZoomingOut == true || zoomDirection == -1)) { - this._aggregateHubs(force); - this._updateNodeIndexList(); - } - // we now reduce chains. - if (detectedZoomingOut == true || zoomDirection == -1) { // zoom out - this.handleChains(); - this._updateNodeIndexList(); - } + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + exports.selectEdges = function (selection) { + var i, iMax, id; - this.previousScale = this.scale; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - // update labels - this.updateLabels(); + // first unselect any selected node + this._unselectAll(true); - // 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 clusters have been made, we normalize the cluster level - this.normalizeClusterLevels(); - } + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - if (doNotStart == false || doNotStart === undefined) { - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); } + this._selectObject(edge, true, true, false, true); } - - this._updateCalculationNodes(); + this.redraw(); }; /** - * This function handles the chains. It is called on every updateClusters(). + * Validate the selection: remove ids of nodes which no longer exist + * @private */ - exports.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) - + exports._updateSelection = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } } }; - /** - * this functions starts clustering by hubs - * The minimum hub threshold is set globally - * - * @private - */ - exports._aggregateHubs = function(force) { - this._getHubSize(); - this._formClustersByHub(force,false); - }; +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + var util = __webpack_require__(1); + var Node = __webpack_require__(56); + var Edge = __webpack_require__(57); + var Hammer = __webpack_require__(19); /** - * This function forces hubs to form. + * clears the toolbar div element of children * + * @private */ - exports.forceAggregateHubs = function(doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - this._aggregateHubs(true); + exports._clearManipulatorBar = function () { + this._recursiveDOMDelete(this.manipulationDiv); + this.manipulationDOM = {}; - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this.updateLabels(); + this._cleanManipulatorHammers(); - this._updateCalculationNodes(); + this._manipulationReleaseOverload = function () {}; + delete this.body.sectors.support.nodes.targetNode; + delete this.body.sectors.support.nodes.targetViaNode; + this.controlNodesActive = false; + this.freezeSimulationEnabled = false; + }; - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; - } - if (doNotStart == false || doNotStart === undefined) { - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); + exports._cleanManipulatorHammers = function () { + // clean hammer bindings + if (this.manipulationHammers.length != 0) { + for (var i = 0; i < this.manipulationHammers.length; i++) { + this.manipulationHammers[i].dispose(); } + this.manipulationHammers = []; } }; /** - * If a cluster takes up more than a set percentage of the screen, open the cluster + * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore + * these functions to their original functionality, we saved them in this.cachedFunctions. + * This function restores these functions to their original function. * * @private */ - exports._openClustersBySize = function() { - if (this.constants.clustering.clusterByZoom == true) { - 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); - } - } - } + exports._restoreOverloadedFunctions = function () { + for (var functionName in this.cachedFunctions) { + if (this.cachedFunctions.hasOwnProperty(functionName)) { + this[functionName] = this.cachedFunctions[functionName]; + delete this.cachedFunctions[functionName]; } } }; - /** - * 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. + * Enable or disable edit-mode. * * @private */ - exports._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._updateCalculationNodes(); + exports._toggleEditMode = function () { + this.editMode = !this.editMode; + var toolbar = this.manipulationDiv; + var closeDiv = this.closeDiv; + var editModeDiv = this.editModeDiv; + if (this.editMode == true) { + toolbar.style.display = "block"; + closeDiv.style.display = "block"; + editModeDiv.style.display = "none"; + this._bindHammerToDiv(closeDiv, "_toggleEditMode"); + } else { + toolbar.style.display = "none"; + closeDiv.style.display = "none"; + editModeDiv.style.display = "block"; } + this._createManipulatorBar(); }; /** - * 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. + * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. * - * @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 */ - exports._expandClusterNode = function(parentNode, recursive, force, openAll) { - // first check if node is a cluster - if (parentNode.clusterSize > 1) { - if (openAll === undefined) { - openAll = false; - } - // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - - recursive = openAll || 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); - } - } - } - } - } + exports._createManipulatorBar = function () { + // remove bound functions + if (this.boundFunction) { + this.off("select", this.boundFunction); } - }; - /** - * 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 - */ - exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) { - var childNode = parentNode.containedNodes[containedNodeId] + this._cleanManipulatorHammers(); - // if child node has been added on smaller scale than current, kick out - if (childNode.formationScale < this.scale || force == true) { - // unselect all selected items - this._unselectAll(); + var locale = this.constants.locales[this.constants.locale]; - // put the child node back in the global nodes object - this.nodes[containedNodeId] = childNode; + if (this.edgeBeingEdited !== undefined) { + this.edgeBeingEdited._disableControlNodes(); + this.edgeBeingEdited = undefined; + this.selectedControlNode = null; + this.controlNodesActive = false; + this._redraw(); + } + + // restore overloaded functions + this._restoreOverloadedFunctions(); + + // resume calculation + this.freezeSimulation(false); + + // reset global variables + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false; + this.manipulationDOM = {}; - // release the contained edges from this childNode back into the global edges - this._releaseContainedEdges(parentNode,childNode); + if (this.editMode == true) { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + } - // reconnect rerouted edges to the childNode - this._connectEdgeBackToChild(parentNode,childNode); + this.manipulationDOM.addNodeSpan = document.createElement("div"); + this.manipulationDOM.addNodeSpan.className = "network-manipulationUI add"; - // validate all edges in dynamicEdges - this._validateEdges(parentNode); + this.manipulationDOM.addNodeLabelSpan = document.createElement("div"); + this.manipulationDOM.addNodeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.addNodeLabelSpan.innerHTML = locale.addNode; + this.manipulationDOM.addNodeSpan.appendChild(this.manipulationDOM.addNodeLabelSpan); - // undo the changes from the clustering operation on the parent node - parentNode.options.mass -= childNode.options.mass; - parentNode.clusterSize -= childNode.clusterSize; - parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*(parentNode.clusterSize-1)); + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; - // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); - childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); + this.manipulationDOM.addEdgeSpan = document.createElement("div"); + this.manipulationDOM.addEdgeSpan.className = "network-manipulationUI connect"; + this.manipulationDOM.addEdgeLabelSpan = document.createElement("div"); + this.manipulationDOM.addEdgeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.addEdgeLabelSpan.innerHTML = locale.addEdge; + this.manipulationDOM.addEdgeSpan.appendChild(this.manipulationDOM.addEdgeLabelSpan); - // remove node from the list - delete parentNode.containedNodes[containedNodeId]; + this.manipulationDiv.appendChild(this.manipulationDOM.addNodeSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.addEdgeSpan); - // 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 (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this.manipulationDOM.seperatorLineDiv2 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv2.className = "network-seperatorLine"; + + this.manipulationDOM.editNodeSpan = document.createElement("div"); + this.manipulationDOM.editNodeSpan.className = "network-manipulationUI edit"; + this.manipulationDOM.editNodeLabelSpan = document.createElement("div"); + this.manipulationDOM.editNodeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.editNodeLabelSpan.innerHTML = locale.editNode; + this.manipulationDOM.editNodeSpan.appendChild(this.manipulationDOM.editNodeLabelSpan); + + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv2); + this.manipulationDiv.appendChild(this.manipulationDOM.editNodeSpan); + } else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this.manipulationDOM.seperatorLineDiv3 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv3.className = "network-seperatorLine"; + + this.manipulationDOM.editEdgeSpan = document.createElement("div"); + this.manipulationDOM.editEdgeSpan.className = "network-manipulationUI edit"; + this.manipulationDOM.editEdgeLabelSpan = document.createElement("div"); + this.manipulationDOM.editEdgeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.editEdgeLabelSpan.innerHTML = locale.editEdge; + this.manipulationDOM.editEdgeSpan.appendChild(this.manipulationDOM.editEdgeLabelSpan); + + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv3); + this.manipulationDiv.appendChild(this.manipulationDOM.editEdgeSpan); } - // if there are no others, remove the cluster session from the list - if (othersPresent == false) { - parentNode.clusterSessions.pop(); + if (this._selectionIsEmpty() == false) { + this.manipulationDOM.seperatorLineDiv4 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv4.className = "network-seperatorLine"; + + this.manipulationDOM.deleteSpan = document.createElement("div"); + this.manipulationDOM.deleteSpan.className = "network-manipulationUI delete"; + this.manipulationDOM.deleteLabelSpan = document.createElement("div"); + this.manipulationDOM.deleteLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.deleteLabelSpan.innerHTML = locale.del; + this.manipulationDOM.deleteSpan.appendChild(this.manipulationDOM.deleteLabelSpan); + + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv4); + this.manipulationDiv.appendChild(this.manipulationDOM.deleteSpan); } - this._repositionBezierNodes(childNode); - // this._repositionBezierNodes(parentNode); + // bind the icons + this._bindHammerToDiv(this.manipulationDOM.addNodeSpan, "_createAddNodeToolbar"); + this._bindHammerToDiv(this.manipulationDOM.addEdgeSpan, "_createAddEdgeToolbar"); + this._bindHammerToDiv(this.closeDiv, "_toggleEditMode"); + + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this._bindHammerToDiv(this.manipulationDOM.editNodeSpan, "_editNode"); + } else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this._bindHammerToDiv(this.manipulationDOM.editEdgeSpan, "_createEditEdgeToolbar"); + } + if (this._selectionIsEmpty() == false) { + this._bindHammerToDiv(this.manipulationDOM.deleteSpan, "_deleteSelected"); + } - // remove the clusterSession from the child node - childNode.clusterSession = 0; + var me = this; + this.boundFunction = me._createManipulatorBar; + this.on("select", this.boundFunction); + } else { + while (this.editModeDiv.hasChildNodes()) { + this.editModeDiv.removeChild(this.editModeDiv.firstChild); + } - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); + this.manipulationDOM.editModeSpan = document.createElement("div"); + this.manipulationDOM.editModeSpan.className = "network-manipulationUI edit editmode"; + this.manipulationDOM.editModeLabelSpan = document.createElement("div"); + this.manipulationDOM.editModeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.editModeLabelSpan.innerHTML = locale.edit; + this.manipulationDOM.editModeSpan.appendChild(this.manipulationDOM.editModeLabelSpan); - // restart the simulation to reorganise all nodes - this.moving = true; - } + this.editModeDiv.appendChild(this.manipulationDOM.editModeSpan); - // check if a further expansion step is possible if recursivity is enabled - if (recursive == true) { - this._expandClusterNode(childNode,recursive,force,openAll); + this._bindHammerToDiv(this.manipulationDOM.editModeSpan, "_toggleEditMode"); } }; - /** - * position the bezier nodes at the center of the edges - * - * @param node - * @private - */ - exports._repositionBezierNodes = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - node.dynamicEdges[i].positionBezierNode(); - } + exports._bindHammerToDiv = function (domElement, funct) { + var hammer = Hammer(domElement, { prevent_default: true }); + hammer.on("touch", this[funct].bind(this)); + this.manipulationHammers.push(hammer); }; /** - * 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 + * Create the toolbar for adding Nodes * * @private - * @param {Boolean} force */ - exports._formClusters = function(force) { - if (force == false) { - if (this.constants.clustering.clusterByZoom == true) { - this._formClustersByZoom(); - } - } - else { - this._forceClustersByZoom(); + exports._createAddNodeToolbar = function () { + // clear the toolbar + this._clearManipulatorBar(); + if (this.boundFunction) { + this.off("select", this.boundFunction); } - }; + var locale = this.constants.locales[this.constants.locale]; - /** - * This function handles the clustering by zooming out, this is based on a minimum edge distance - * - * @private - */ - exports._formClustersByZoom = function() { - var dx,dy,length; - var 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.options.mass > edge.from.options.mass) { - parentNode = edge.to; - childNode = edge.from; - } + this.manipulationDOM = {}; + this.manipulationDOM.backSpan = document.createElement("div"); + this.manipulationDOM.backSpan.className = "network-manipulationUI back"; + this.manipulationDOM.backLabelSpan = document.createElement("div"); + this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.backLabelSpan.innerHTML = locale.back; + this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); + + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + + this.manipulationDOM.descriptionSpan = document.createElement("div"); + this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; + this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); + this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.addDescription; + this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); + + this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); - if (childNode.dynamicEdges.length == 1) { - this._addToCluster(parentNode,childNode,false); - } - else if (parentNode.dynamicEdges.length == 1) { - this._addToCluster(childNode,parentNode,false); - } - } - } - } - } - } + // bind the icon + this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); + + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + var me = this; + this.boundFunction = me._addNode; + this.on("select", this.boundFunction); }; + /** - * This function forces the network to cluster all nodes with only one connecting edge to their - * connected node. + * create the toolbar to connect nodes * * @private */ - exports._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.dynamicEdges.length == 1) { - 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.options.mass > childNode.options.mass) { - this._addToCluster(parentNode,childNode,true); - } - else { - this._addToCluster(childNode,parentNode,true); - } - } - } - } + exports._createAddEdgeToolbar = function () { + // clear the toolbar + this._clearManipulatorBar(); + this._unselectAll(true); + this.freezeSimulationEnabled = true; + + if (this.boundFunction) { + this.off("select", this.boundFunction); } - }; + var locale = this.constants.locales[this.constants.locale]; - /** - * To keep the nodes of roughly equal size we normalize the cluster levels. - * This function clusters a node to its smallest connected neighbour. - * - * @param node - * @private - */ - exports._clusterToSmallestNeighbour = function(node) { - var smallestNeighbour = -1; - var smallestNeighbourNode = null; - for (var i = 0; i < node.dynamicEdges.length; i++) { - if (node.dynamicEdges[i] !== undefined) { - var neighbour = null; - if (node.dynamicEdges[i].fromId != node.id) { - neighbour = node.dynamicEdges[i].from; - } - else if (node.dynamicEdges[i].toId != node.id) { - neighbour = node.dynamicEdges[i].to; - } + this.forceAppendSelection = false; + this.blockConnectingEdgeSelection = true; + this.manipulationDOM = {}; + this.manipulationDOM.backSpan = document.createElement("div"); + this.manipulationDOM.backSpan.className = "network-manipulationUI back"; + this.manipulationDOM.backLabelSpan = document.createElement("div"); + this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.backLabelSpan.innerHTML = locale.back; + this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); + + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + + this.manipulationDOM.descriptionSpan = document.createElement("div"); + this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; + this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); + this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.edgeDescription; + this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); + + this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); - if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { - smallestNeighbour = neighbour.clusterSessions.length; - smallestNeighbourNode = neighbour; - } - } - } + // bind the icon + this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); - if (neighbour != null && this.nodes[neighbour.id] !== undefined) { - this._addToCluster(neighbour, node, true); - } - }; + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + var me = this; + this.boundFunction = me._handleConnect; + this.on("select", this.boundFunction); + // temporarily overload functions + this.cachedFunctions._handleTouch = this._handleTouch; + this.cachedFunctions._manipulationReleaseOverload = this._manipulationReleaseOverload; + this.cachedFunctions._handleDragStart = this._handleDragStart; + this.cachedFunctions._handleDragEnd = this._handleDragEnd; + this.cachedFunctions._handleOnHold = this._handleOnHold; + this._handleTouch = this._handleConnect; + this._manipulationReleaseOverload = function () {}; + this._handleOnHold = function () {}; + this._handleDragStart = function () {}; + this._handleDragEnd = this._finishConnect; - /** - * 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 - */ - exports._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); - } - } + // redraw to show the unselect + this._redraw(); }; /** - * This function forms a cluster from a specific preselected hub node + * create the toolbar to edit edges * - * @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 */ - exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { - if (absorptionSizeOffset === undefined) { - absorptionSizeOffset = 0; + exports._createEditEdgeToolbar = function () { + // clear the toolbar + this._clearManipulatorBar(); + this.controlNodesActive = true; + + if (this.boundFunction) { + this.off("select", this.boundFunction); } - //this.hubThreshold = 43 - //if (hubNode.dynamicEdgesLength < 0) { - // console.error(hubNode.dynamicEdgesLength, this.hubThreshold, onlyEqual) - //} - // we decide if the node is a hub - if ((hubNode.dynamicEdges.length >= this.hubThreshold && onlyEqual == false) || - (hubNode.dynamicEdges.length == 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 forced, 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) { - var children = []; - var childrenIds = {}; - // we loop over all edges INITIALLY connected to this hub to get a list of the childNodes - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIdarray[j]]; - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - if (childrenIds[childNode.id] === undefined) { - childrenIds[childNode.id] = true; - children.push(childNode); - } - } + this.edgeBeingEdited = this._getSelectedEdge(); + this.edgeBeingEdited._enableControlNodes(); - for (j = 0; j < children.length; j++) { - var childNode = children[j]; - // 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); + var locale = this.constants.locales[this.constants.locale]; - } - else { - //console.log("WILL NOT MERGE:",childNode.dynamicEdges.length , (this.hubThreshold + absorptionSizeOffset)) - } - } + this.manipulationDOM = {}; + this.manipulationDOM.backSpan = document.createElement("div"); + this.manipulationDOM.backSpan.className = "network-manipulationUI back"; + this.manipulationDOM.backLabelSpan = document.createElement("div"); + this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.backLabelSpan.innerHTML = locale.back; + this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); + + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + + this.manipulationDOM.descriptionSpan = document.createElement("div"); + this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; + this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); + this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.editEdgeDescription; + this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); + + this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); - } - } - }; + // bind the icon + this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); + + // temporarily overload functions + this.cachedFunctions._handleTouch = this._handleTouch; + this.cachedFunctions._manipulationReleaseOverload = this._manipulationReleaseOverload; + this.cachedFunctions._handleTap = this._handleTap; + this.cachedFunctions._handleDragStart = this._handleDragStart; + this.cachedFunctions._handleOnDrag = this._handleOnDrag; + this._handleTouch = this._selectControlNode; + this._handleTap = function () {}; + this._handleOnDrag = this._controlNodeDrag; + this._handleDragStart = function () {}; + this._manipulationReleaseOverload = this._releaseControlNode; + // redraw to show the unselect + this._redraw(); + }; /** - * This function adds the child node to the parent node, creating a cluster if it is not already. + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. * - * @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 */ - exports._addToCluster = function(parentNode, childNode, force) { - // join child node in the parent node - parentNode.containedNodes[childNode.id] = childNode; - //console.log(parentNode.id, childNode.id) - // 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 - //console.log("COLLECT",parentNode.id, childNode.id, edge.toId, edge.fromId) - this._addToContainedEdges(parentNode,childNode,edge); - } - else { - //console.log("REWIRE",parentNode.id, childNode.id, edge.toId, edge.fromId) - 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.options.mass; - childNode.clusterSession = this.clusterSession; - parentNode.options.mass += childNode.options.mass; - parentNode.clusterSize += childNode.clusterSize; - parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.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 = 0; - } - else { - parentNode.formationScale = this.scale; // The latest child has been added on this scale + exports._selectControlNode = function (pointer) { + this.edgeBeingEdited.controlNodes.from.unselect(); + this.edgeBeingEdited.controlNodes.to.unselect(); + this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x), this._YconvertDOMtoCanvas(pointer.y)); + if (this.selectedControlNode !== null) { + this.selectedControlNode.select(); + this.freezeSimulation(true); } - - // 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._redraw(); }; /** - * This adds an edge from the childNode to the contained edges of the parent node + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object * @private */ - exports._addToContainedEdges = function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - if (parentNode.containedEdges[childNode.id] === undefined) { - parentNode.containedEdges[childNode.id] = [] + exports._controlNodeDrag = function (event) { + var pointer = this._getPointer(event.gesture.center); + if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { + this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); + this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); } - // add this edge to the list - parentNode.containedEdges[childNode.id].push(edge); + this._redraw(); + }; - // 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; + /** + * + * @param pointer + * @private + */ + exports._releaseControlNode = function (pointer) { + var newNode = this._getNodeAt(pointer); + if (newNode !== null) { + if (this.edgeBeingEdited.controlNodes.from.selected == true) { + this.edgeBeingEdited._restoreControlNodes(); + this._editEdge(newNode.id, this.edgeBeingEdited.to.id); + this.edgeBeingEdited.controlNodes.from.unselect(); + } + if (this.edgeBeingEdited.controlNodes.to.selected == true) { + this.edgeBeingEdited._restoreControlNodes(); + this._editEdge(this.edgeBeingEdited.from.id, newNode.id); + this.edgeBeingEdited.controlNodes.to.unselect(); } + } else { + this.edgeBeingEdited._restoreControlNodes(); } + this.freezeSimulation(false); + this._redraw(); }; /** - * This 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. + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. * - * @param {Node} parentNode | Node object - * @param {Node} childNode | Node object - * @param {Edge} edge | Edge object * @private */ - exports._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; - } + exports._handleConnect = function (pointer) { + if (this._getSelectedNodeCount() == 0) { + var node = this._getNodeAt(pointer); + + if (node != null) { + if (this.isCluster(node.id) == true) { + alert(this.constants.locales[this.constants.locale].createEdgeError); + } else { + this._selectObject(node, false); + var supportNodes = this.body.sectors.support.nodes; + + // create a node the temporary line can look at + supportNodes.targetNode = this.body.functions.createNode({ id: "targetNode" }); + var targetNode = supportNodes.targetNode; + targetNode.x = node.x; + targetNode.y = node.y; + + // create a temporary edge + this.body.edges.connectionEdge = this.body.functions.createEdge({ id: "connectionEdge", from: node.id, to: targetNode.id }); + var connectionEdge = this.body.edges.connectionEdge; + connectionEdge.from = node; + connectionEdge.connected = true; + connectionEdge.options.smoothCurves = { enabled: true, + dynamic: false, + type: "continuous", + roundness: 0.5 + }; + connectionEdge.selected = true; + connectionEdge.to = targetNode; - this._addToReroutedEdges(parentNode,childNode,edge); + this.cachedFunctions._handleOnDrag = this._handleOnDrag; + var me = this; + this._handleOnDrag = function (event) { + var pointer = me._getPointer(event.gesture.center); + var connectionEdge = me.body.edges.connectionEdge; + connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x); + connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y); + me._redraw(); + }; + } + } } }; + exports._finishConnect = function (event) { + if (this._getSelectedNodeCount() == 1) { + var pointer = this._getPointer(event.gesture.center); + // restore the drag function + this._handleOnDrag = this.cachedFunctions._handleOnDrag; + delete this.cachedFunctions._handleOnDrag; - /** - * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain - * these edges inside of the cluster. - * - * @param parentNode - * @param childNode - * @private - */ - exports._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); + // remember the edge id + var connectFromId = this.body.edges.connectionEdge.fromId; + + // remove the temporary nodes and edge + delete this.body.edges.connectionEdge; + delete this.body.sectors.support.nodes.targetNode; + delete this.body.sectors.support.nodes.targetViaNode; + + var node = this._getNodeAt(pointer); + if (node != null) { + if (this.isCluster(node.id) === true) { + alert(this.constants.locales[this.constants.locale].createEdgeError); + } else { + this._createEdge(connectFromId, node.id); + this._createManipulatorBar(); + } } + this._unselectAll(); } }; /** - * 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 + * Adds a node on the specified location */ - exports._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] = []; + exports._addNode = function () { + if (this._selectionIsEmpty() && this.editMode == true) { + var positionObject = this._pointerToPositionObject(this.pointerPosition); + var defaultData = { id: util.randomUUID(), x: positionObject.left, y: positionObject.top, label: "new", allowedToMoveX: true, allowedToMoveY: true }; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function (finalizedData) { + me.body.data.nodes.add(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for add does not support two arguments (data,callback)"); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } else { + this.body.data.nodes.add(defaultData); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } } - 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. + * connect two nodes with a new edge. * - * @param parentNode | Node object - * @param childNode | Node object * @private */ - exports._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; - } + exports._createEdge = function (sourceNodeId, targetNodeId) { + if (this.editMode == true) { + var defaultData = { from: sourceNodeId, to: targetNodeId }; + if (this.triggerFunctions.connect) { + if (this.triggerFunctions.connect.length == 2) { + var me = this; + this.triggerFunctions.connect(defaultData, function (finalizedData) { + me.body.data.edges.add(finalizedData); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for connect does not support two arguments (data,callback)"); + this.moving = true; + this.start(); } + } else { + this.body.data.edges.add(defaultData); + this.moving = true; + this.start(); } - // 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 + * connect two nodes with a new edge. * - * @param parentNode | Node object * @private */ - exports._validateEdges = function(parentNode) { - var dynamicEdges = [] - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - if (parentNode.id == edge.toId || parentNode.id == edge.fromId) { - dynamicEdges.push(edge); + exports._editEdge = function (sourceNodeId, targetNodeId) { + if (this.editMode == true) { + var defaultData = { id: this.edgeBeingEdited.id, from: sourceNodeId, to: targetNodeId }; + console.log(defaultData); + if (this.triggerFunctions.editEdge) { + if (this.triggerFunctions.editEdge.length == 2) { + var me = this; + this.triggerFunctions.editEdge(defaultData, function (finalizedData) { + me.body.data.edges.update(finalizedData); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + this.moving = true; + this.start(); + } + } else { + this.body.data.edges.update(defaultData); + this.moving = true; + this.start(); } } - parentNode.dynamicEdges = dynamicEdges; }; - /** - * This function released the contained edges back into the global domain and puts them back into the - * dynamic edges of both parent and child. + * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. * - * @param {Node} parentNode | - * @param {Node} childNode | * @private */ - exports._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); + exports._editNode = function () { + if (this.triggerFunctions.edit && this.editMode == true) { + var node = this._getSelectedNode(); + var data = { id: node.id, + label: node.label, + group: node.options.group, + shape: node.options.shape, + color: { + background: node.options.color.background, + border: node.options.color.border, + highlight: { + background: node.options.color.highlight.background, + border: node.options.color.highlight.border + } + } }; + if (this.triggerFunctions.edit.length == 2) { + var me = this; + this.triggerFunctions.edit(data, function (finalizedData) { + me.body.data.nodes.update(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + } + } else { + throw new Error("No edit function has been bound to this button"); } - // 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) + * delete everything in the selection + * + * @private */ - exports.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); + exports._deleteSelected = function () { + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.del) { + var me = this; + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (this.triggerFunctions.del.length == 2) { + this.triggerFunctions.del(data, function (finalizedData) { + me.body.data.edges.remove(finalizedData.edges); + me.body.data.nodes.remove(finalizedData.nodes); + me._unselectAll(); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for delete does not support two arguments (data, callback)"); } + } else { + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); + this._unselectAll(); + this.moving = true; + this.start(); } + } else { + alert(this.constants.locales[this.constants.locale].deleteClusterError); } } - - // /* Debug Override */ - // for (nodeId in this.nodes) { - // if (this.nodes.hasOwnProperty(nodeId)) { - // node = this.nodes[nodeId]; - // node.label = String(node.clusterSize + ":" + node.dynamicEdges.length); - // } - // } - }; +/***/ }, +/* 62 */ +/***/ function(module, exports, __webpack_require__) { - /** - * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes - * if the rest of the nodes are already a few cluster levels in. - * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not - * clustered enough to the clusterToSmallestNeighbours function. - */ - exports.normalizeClusterLevels = function() { - var maxLevel = 0; - var minLevel = 1e9; - var clusterLevel = 0; - var nodeId; + "use strict"; - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - clusterLevel = this.nodes[nodeId].clusterSessions.length; - if (maxLevel < clusterLevel) {maxLevel = clusterLevel;} - if (minLevel > clusterLevel) {minLevel = clusterLevel;} - } - } + var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); - if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { - var amountOfNodes = this.nodeIndices.length; - var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].clusterSessions.length < targetLevel) { - this._clusterToSmallestNeighbour(this.nodes[nodeId]); - } - } - } - this._updateNodeIndexList(); - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; + exports._cleanNavigation = function () { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].dispose(); } + this.navigationHammers = []; } - }; - - - - /** - * This 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 - */ - exports._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._navigationReleaseOverload = function () {}; - /** - * 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. - * - */ - exports.repositionNodes = function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - if ((node.xFixed == false || node.yFixed == false)) { - var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.options.mass); - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} - this._repositionBezierNodes(node); - } + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { + this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); } }; - /** - * 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%) + * 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 */ - exports._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.dynamicEdges.length > largestHub) { - largestHub = node.dynamicEdges.length; - } - average += node.dynamicEdges.length; - averageSquared += Math.pow(node.dynamicEdges.length,2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; + exports._loadNavigationElements = function () { + this._cleanNavigation(); - var variance = averageSquared - Math.pow(average,2); + this.navigationDOM = {}; + var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; + var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - var standardDeviation = Math.sqrt(variance); + this.navigationDOM.wrapper = document.createElement("div"); + this.frame.appendChild(this.navigationDOM.wrapper); - this.hubThreshold = Math.floor(average + 2*standardDeviation); + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDOM[navigationDivs[i]] = document.createElement("div"); + this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; + this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); - // always have at least one to cluster - if (this.hubThreshold > largestHub) { - this.hubThreshold = largestHub; + var hammer = Hammer(this.navigationDOM[navigationDivs[i]], { prevent_default: true }); + hammer.on("touch", this[navigationDivActions[i]].bind(this)); + this.navigationHammers.push(hammer); } - // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); - // console.log("hubThreshold:",this.hubThreshold); + this._navigationReleaseOverload = this._stopMovement; }; /** - * 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. + * this stops all movement induced by the navigation buttons * - * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce * @private */ - exports._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].dynamicEdges.length == 2) { - if (reduceAmount > 0) { - this._formClusterFromHub(this.nodes[nodeId],true,true,1); - reduceAmount -= 1; - } - } - } - } + exports._zoomExtent = function (event) { + this.zoomExtent({ duration: 700 }); + event.stopPropagation(); }; /** - * 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. + * this stops all movement induced by the navigation buttons * * @private */ - exports._getChainFraction = function() { - var chains = 0; - var total = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdges.length == 2) { - chains += 1; - } - total += 1; - } - } - return chains/total; + exports._stopMovement = function () { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); }; -/***/ }, -/* 65 */ -/***/ function(module, exports, __webpack_require__) { + /** + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. + * + * @private + */ + exports._moveUp = function (event) { + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - var util = __webpack_require__(1); - var Node = __webpack_require__(56); - /** - * Creation of the SectorMixin var. - * - * This contains all the functions the Network object can use to employ the sector system. - * The sector system is always used by Network, 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. - */ + /** + * move the screen down + * @private + */ + exports._moveDown = function (event) { + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; + /** - * This function is only called by the setData function of the Network object. - * This loads the global references into the active sector. This initializes the sector. - * + * move the screen left * @private */ - exports._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; + exports._moveLeft = function (event) { + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); }; /** - * /** - * This 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" + * move the screen right * @private */ - exports._switchToSector = function(sectorId, sectorType) { - if (sectorType === undefined || sectorType == "active") { - this._switchToActiveSector(sectorId); - } - else { - this._switchToFrozenSector(sectorId); - } + exports._moveRight = function (event) { + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. - * - * @param sectorId + * Zoom in, using the same method as the movement. * @private */ - exports._switchToActiveSector = function(sectorId) { - this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["active"][sectorId]["nodes"]; - this.edges = this.sectors["active"][sectorId]["edges"]; + exports._zoomIn = function (event) { + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. - * + * Zoom out * @private */ - exports._switchToSupportSector = function() { - this.nodeIndices = this.sectors["support"]["nodeIndices"]; - this.nodes = this.sectors["support"]["nodes"]; - this.edges = this.sectors["support"]["edges"]; + exports._zoomOut = function (event) { + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied frozen sector. - * - * @param sectorId + * Stop zooming and unhighlight the zoom controls * @private */ - exports._switchToFrozenSector = function(sectorId) { - this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["frozen"][sectorId]["nodes"]; - this.edges = this.sectors["frozen"][sectorId]["edges"]; + exports._stopZoom = function (event) { + this.zoomIncrement = 0; + event && event.preventDefault(); }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the currently active sector. - * + * Stop moving in the Y direction and unHighlight the up and down * @private */ - exports._loadLatestSector = function() { - this._switchToSector(this._sector()); + exports._yStopMoving = function (event) { + this.yIncrement = 0; + event && event.preventDefault(); }; /** - * This function returns the currently active sector Id - * - * @returns {String} + * Stop moving in the X direction and unHighlight left and right. * @private */ - exports._sector = function() { - return this.activeSector[this.activeSector.length-1]; + exports._xStopMoving = function (event) { + this.xIncrement = 0; + event && event.preventDefault(); }; +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + exports._resetLevels = function () { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.preassignedLevel == false) { + node.level = -1; + node.hierarchyEnumerated = false; + } + } + } + }; /** - * This function returns the previously active sector Id + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly * - * @returns {String} * @private */ - exports._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.'); + exports._setupHierarchicalLayout = function () { + if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var hubsize = 0; + var node, nodeId; + var definedLevel = false; + var undefinedLevel = false; + + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.level != -1) { + definedLevel = true; + } else { + undefinedLevel = true; + } + if (hubsize < node.edges.length) { + hubsize = node.edges.length; + } + } + } + + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); + if (!this.constants.clustering.enabled) { + this.start(); + } + } else { + // setup the system to use hierarchical method. + this._changeConstants(); + + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.constants.hierarchicalLayout.layout == "hubsize") { + this._determineLevels(hubsize); + } else { + this._determineLevelsDirected(false); + } + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); + + // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize. + this._placeNodesByHierarchy(distribution); + } } }; /** - * 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. + * This function places the nodes on the canvas based on the hierarchial distribution. * - * @param newId + * @param {Object} distribution | obtained by the function this._getDistribution() * @private */ - exports._setActiveSector = function(newId) { - this.activeSector.push(newId); + exports._placeNodesByHierarchy = function (distribution) { + var nodeId, node; + + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { + node = distribution[level].nodes[nodeId]; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (node.xFixed) { + node.x = distribution[level].minPos; + node.xFixed = false; + + distribution[level].minPos += distribution[level].nodeSpacing; + } + } else { + if (node.yFixed) { + node.y = distribution[level].minPos; + node.yFixed = false; + + distribution[level].minPos += distribution[level].nodeSpacing; + } + } + this._placeBranchNodes(node.edges, node.id, distribution, node.level); + } + } + } + } + + // stabilize the system after positioning. This function calls zoomExtent. + this._stabilize(); }; /** - * We remove the currently active sector id from the active sector stack. This happens when - * we reactivate the previously active sector + * This function get the distribution of levels based on hubsize * + * @returns {Object} * @private */ - exports._forgetLastSector = function() { - this.activeSector.pop(); + exports._getDistribution = function () { + var distribution = {}; + var nodeId, node, level; + + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + node.xFixed = true; + node.yFixed = true; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + node.y = this.constants.hierarchicalLayout.levelSeparation * node.level; + } else { + node.x = this.constants.hierarchicalLayout.levelSeparation * node.level; + } + if (distribution[node.level] === undefined) { + distribution[node.level] = { amount: 0, nodes: {}, minPos: 0, nodeSpacing: 0 }; + } + distribution[node.level].amount += 1; + distribution[node.level].nodes[nodeId] = node; + } + } + + // determine the largest amount of nodes of all levels + var maxCount = 0; + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + if (maxCount < distribution[level].amount) { + maxCount = distribution[level].amount; + } + } + } + + // set the initial position and spacing of each nodes accordingly + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; + distribution[level].nodeSpacing /= distribution[level].amount + 1; + distribution[level].minPos = distribution[level].nodeSpacing - 0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing; + } + } + + return distribution; }; /** - * This function creates a new active sector with the supplied newId. This newId - * is the expanding node id. + * this function allocates nodes in levels based on the recursive branching from the largest hubs. * - * @param {String} newId | Id of the new active sector + * @param hubsize * @private */ - exports._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; + exports._determineLevels = function (hubsize) { + var nodeId, node; + + // determine hubs + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length == hubsize) { + node.level = 0; + } + } + } + + // branch from hubs + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.level == 0) { + this._setLevel(1, node.edges, node.id); + } + } + } }; + /** - * This function removes the currently active sector. This is called when we create a new - * active sector. + * this function allocates nodes in levels based on the direction of the edges * - * @param {String} sectorId | Id of the active sector that will be removed + * @param hubsize * @private */ - exports._deleteActiveSector = function(sectorId) { - delete this.sectors["active"][sectorId]; + exports._determineLevelsDirected = function () { + var nodeId, node, firstNode; + var minLevel = 10000; + + // set first node to source + firstNode = this.body.nodes[this.nodeIndices[0]]; + firstNode.level = minLevel; + this._setLevelDirected(minLevel, firstNode.edges, firstNode.id); + + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + minLevel = node.level < minLevel ? node.level : minLevel; + } + } + + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + node.level -= minLevel; + } + } }; /** - * This function removes the currently active sector. This is called when we reactivate - * the previously active sector. + * Since hierarchical layout does not support: + * - smooth curves (based on the physics), + * - clustering (based on dynamic node counts) + * + * We disable both features so there will be no problems. * - * @param {String} sectorId | Id of the active sector that will be removed * @private */ - exports._deleteFrozenSector = function(sectorId) { - delete this.sectors["frozen"][sectorId]; + exports._changeConstants = function () { + this.constants.clustering.enabled = false; + this.constants.physics.barnesHut.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this._loadSelectedForceSolver(); + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; + } + this._configureSmoothCurves(); + + var config = this.constants.hierarchicalLayout; + config.levelSeparation = Math.abs(config.levelSeparation); + if (config.direction == "RL" || config.direction == "DU") { + config.levelSeparation *= -1; + } + + if (config.direction == "RL" || config.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; + } + } else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; + } + } }; /** - * Freezing an active sector means moving it from the "active" object to the "frozen" object. - * We copy the references, then delete the active entree. + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. * - * @param sectorId + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @private */ - exports._freezeSector = function(sectorId) { - // we move the set references from the active to the frozen stack. - this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; + exports._placeBranchNodes = function (edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } + + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + var nodeMoved = false; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (childNode.xFixed && childNode.level > parentLevel) { + childNode.xFixed = false; + childNode.x = distribution[childNode.level].minPos; + nodeMoved = true; + } + } else { + if (childNode.yFixed && childNode.level > parentLevel) { + childNode.yFixed = false; + childNode.y = distribution[childNode.level].minPos; + nodeMoved = true; + } + } - // we have moved the sector data into the frozen set, we now remove it from the active set - this._deleteActiveSector(sectorId); + if (nodeMoved == true) { + distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNode.level); + } + } + } }; /** - * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" - * object to the "active" object. + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. * - * @param sectorId + * @param level + * @param edges + * @param parentId * @private */ - exports._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); + exports._setLevel = function (level, edges, parentId) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } + if (childNode.level == -1 || childNode.level > level) { + childNode.level = level; + if (childNode.edges.length > 1) { + this._setLevel(level + 1, childNode.edges, childNode.id); + } + } + } }; /** - * 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. + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * - * @param sectorId + * @param level + * @param edges + * @param parentId * @private */ - exports._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]; + exports._setLevelDirected = function (level, edges, parentId) { + this.body.nodes[parentId].hierarchyEnumerated = true; + var childNode, direction; + for (var i = 0; i < edges.length; i++) { + direction = 1; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + direction = -1; + } else { + childNode = edges[i].to; + } + if (childNode.level == -1) { + childNode.level = level + direction; } } - // 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]; + for (var i = 0; i < edges.length; i++) { + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; } - } - // merge the nodeIndices - for (var i = 0; i < this.nodeIndices.length; i++) { - this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); + if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { + this._setLevelDirected(childNode.level, childNode.edges, childNode.id); + } } }; /** - * 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. + * Unfix nodes * * @private */ - exports._collapseThisToSingleCluster = function() { - this.clusterToFit(1,false); + exports._restoreNodes = function () { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].xFixed = false; + this.body.nodes[nodeId].yFixed = false; + } + } }; +/***/ }, +/* 64 */ +/***/ function(module, exports, __webpack_require__) { - /** - * We create a new active sector from the node that we want to open. - * - * @param node - * @private - */ - exports._addSector = function(node) { - // this is the currently active sector - var sector = this._sector(); + "use strict"; - // // 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!!"); - // } + // English + exports.en = { + edit: "Edit", + del: "Delete selected", + back: "Back", + addNode: "Add Node", + addEdge: "Add Edge", + editNode: "Edit Node", + editEdge: "Edit Edge", + addDescription: "Click in an empty space to place a new node.", + edgeDescription: "Click on a node and drag the edge to another node to connect them.", + editEdgeDescription: "Click on the control points and drag them to a node to connect to it.", + createEdgeError: "Cannot link edges to a cluster.", + deleteClusterError: "Clusters cannot be deleted." + }; + exports.en_EN = exports.en; + exports.en_US = exports.en; - // 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]; + // Dutch + exports.nl = { + edit: "Wijzigen", + del: "Selectie verwijderen", + back: "Terug", + addNode: "Node toevoegen", + addEdge: "Link toevoegen", + editNode: "Node wijzigen", + editEdge: "Link wijzigen", + addDescription: "Klik op een leeg gebied om een nieuwe node te maken.", + edgeDescription: "Klik op een node en sleep de link naar een andere node om ze te verbinden.", + editEdgeDescription: "Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.", + createEdgeError: "Kan geen link maken naar een cluster.", + deleteClusterError: "Clusters kunnen niet worden verwijderd." + }; + exports.nl_NL = exports.nl; + exports.nl_BE = exports.nl; - var unqiueIdentifier = util.randomUUID(); +/***/ }, +/* 65 */ +/***/ function(module, exports, __webpack_require__) { - // we fully freeze the currently active sector - this._freezeSector(sector); + "use strict"; - // we create a new active sector. This sector has the Id of the node to ensure uniqueness - this._createNewSector(unqiueIdentifier); + /** + * Canvas shapes used by Network + */ + if (typeof CanvasRenderingContext2D !== "undefined") { + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function (x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2 * Math.PI, false); + }; - // we add the active sector to the sectors array to be able to revert these steps later on - this._setActiveSector(unqiueIdentifier); + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function (x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; - // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier - this._switchToSector(this._sector()); + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function (x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); - // finally we add the node we removed from our previous active sector to the new active sector - this.nodes[node.id] = node; - }; + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; - /** - * 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 - */ - exports._collapseSector = function() { - // the currently active sector - var sector = this._sector(); + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function (x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; - // 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(); + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function (x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); - // we collapse the sector back to a single cluster - this._collapseThisToSingleCluster(); + for (var n = 0; n < 10; n++) { + var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; + this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); + } - // we move the remaining nodes, edges and nodeIndices to the previous sector. - // This previous sector is the one we will reactivate - this._mergeThisWithFrozen(previousSector); + this.closePath(); + }; - // 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); + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { + var r2d = Math.PI / 180; + if (w - 2 * r < 0) { + r = w / 2; + } //ensure that the radius isn't too large for x + if (h - 2 * r < 0) { + r = h / 2; + } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x + r, y); + this.lineTo(x + w - r, y); + this.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); + this.lineTo(x + w, y + h - r); + this.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); + this.lineTo(x + r, y + h); + this.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); + this.lineTo(x, y + r); + this.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); + }; - // we activate the previously active (and currently frozen) sector. - this._activateSector(previousSector); + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function (x, y, w, h) { + var kappa = 0.5522848, + ox = w / 2 * kappa, + // control point offset horizontal + oy = h / 2 * kappa, + // control point offset vertical + xe = x + w, + // x-end + ye = y + h, + // y-end + xm = x + w / 2, + // x-middle + ym = y + h / 2; // y-middle - // we load the references from the newly active sector into the global references - this._switchToSector(previousSector); + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; - // we forget the previously active sector because we reverted to the one before - this._forgetLastSector(); - // finally, we update the node index list. - this._updateNodeIndexList(); - // we refresh the list with calulation nodes and calculation node indices. - this._updateCalculationNodes(); - } - } - }; + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function (x, y, w, h) { + var f = 1 / 3; + var wEllipse = w; + var hEllipse = h * f; + var kappa = 0.5522848, + ox = wEllipse / 2 * kappa, + // control point offset horizontal + oy = hEllipse / 2 * kappa, + // control point offset vertical + xe = x + wEllipse, + // x-end + ye = y + hEllipse, + // y-end + xm = x + wEllipse / 2, + // x-middle + ym = y + hEllipse / 2, + // y-middle + ymb = y + (h - hEllipse / 2), + // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse - /** - * This 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 Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - exports._doInAllActiveSectors = function(runFunction,argument) { - var returnValues = []; - 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); - returnValues.push( 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) { - returnValues.push( this[runFunction](args[0],args[1]) ); - } - else { - returnValues.push( this[runFunction](argument) ); - } - } - } - } - // we revert the global references back to our active sector - this._loadLatestSector(); - return returnValues; - }; + this.beginPath(); + this.moveTo(xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - /** - * 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 Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - exports._doInSupportSector = function(runFunction,argument) { - var returnValues = false; - if (argument === undefined) { - this._switchToSupportSector(); - returnValues = this[runFunction](); - } - else { - this._switchToSupportSector(); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - returnValues = this[runFunction](args[0],args[1]); - } - else { - returnValues = this[runFunction](argument); - } - } - // we revert the global references back to our active sector - this._loadLatestSector(); - return returnValues; - }; + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.lineTo(xe, ymb); - /** - * 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 Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - exports._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.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + this.lineTo(x, ym); + }; - /** - * 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 Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - exports._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); - } - } - }; + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function (x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); - /** - * This 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 - */ - exports._clearNodeIndexList = function() { - var sector = this._sector(); - this.sectors["active"][sector]["nodeIndices"] = []; - this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; - }; + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); - /** - * Draw the encompassing sector node - * - * @param ctx - * @param sectorType - * @private - */ - exports._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.options.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); - node.setScale(this.scale); - node._drawCircle(ctx); - } - } - } - }; + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); - exports._drawAllSectorNodes = function(ctx) { - this._drawSectorNodes(ctx,"frozen"); - this._drawSectorNodes(ctx,"active"); - this._loadLatestSector(); - }; + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + /** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ + CanvasRenderingContext2D.prototype.dashedLine = function (x, y, x2, y2, dashArray) { + if (!dashArray) dashArray = [10, 5]; + if (dashLength == 0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = x2 - x, + dy = y2 - y; + var slope = dy / dx; + var distRemaining = Math.sqrt(dx * dx + dy * dy); + var dashIndex = 0, + draw = true; + while (distRemaining >= 0.1) { + var dashLength = dashArray[dashIndex++ % dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope)); + if (dx < 0) xStep = -xStep; + x += xStep; + y += slope * xStep; + this[draw ? "lineTo" : "moveTo"](x, y); + distRemaining -= dashLength; + draw = !draw; + } + }; + // TODO: add diamond shape + } /***/ }, /* 66 */ /***/ function(module, exports, __webpack_require__) { - var Node = __webpack_require__(56); + "use strict"; - /** - * This function can be called from the _doInAllSectors function - * - * @param object - * @param overlappingNodes - * @private - */ - exports._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); - } - } - } - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * 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 + * Created by Alex on 2/23/2015. */ - exports._getAllNodesOverlappingWith = function (object) { - var overlappingNodes = []; - this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; - }; + var BarnesHutSolver = __webpack_require__(67).BarnesHutSolver; + var Repulsion = __webpack_require__(68).Repulsion; + var HierarchicalRepulsion = __webpack_require__(69).HierarchicalRepulsion; + var SpringSolver = __webpack_require__(70).SpringSolver; + var HierarchicalSpringSolver = __webpack_require__(71).HierarchicalSpringSolver; + var CentralGravitySolver = __webpack_require__(72).CentralGravitySolver; - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - exports._pointerToPositionObject = function(pointer) { - var x = this._XconvertDOMtoCanvas(pointer.x); - var y = this._YconvertDOMtoCanvas(pointer.y); - return { - left: x, - top: y, - right: x, - bottom: y - }; - }; + var util = __webpack_require__(1); - /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private - */ - exports._getNodeAt = function (pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + var PhysicsEngine = (function () { + function PhysicsEngine(body, options) { + var _this = this; + _classCallCheck(this, PhysicsEngine); - // 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; - } - }; + this.body = body; + this.physicsBody = { calculationNodes: {}, calculationNodeIndices: [], forces: {}, velocities: {} }; + this.scale = 1; + this.viewFunction = undefined; + this.body.emitter.on("_setScale", function (scale) { + return _this.scale = scale; + }); - /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - exports._getEdgesOverlappingWith = function (object, overlappingEdges) { - var edges = this.edges; - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.renderTimer == undefined; + + this.stabilized = false; + this.stabilizationIterations = 0; + + // default options + this.options = { + barnesHut: { + thetaInverted: 1 / 0.5, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.3, + springLength: 95, + springConstant: 0.04, + damping: 0.09 + }, + repulsion: { + centralGravity: 0, + springLength: 200, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + hierarchicalRepulsion: { + centralGravity: 0, + springLength: 100, + springConstant: 0.01, + nodeDistance: 150, + damping: 0.09 + }, + model: "BarnesHut", + timestep: 0.5, + maxVelocity: 50, + minVelocity: 0.1, // px/s + stabilization: { + enabled: true, + iterations: 1000, // maximum number of iteration to stabilize + updateInterval: 100, + onlyDynamicEdges: false, + zoomExtent: true } - } + }; + + this.setOptions(options); } - }; + _prototypeProperties(PhysicsEngine, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + if (typeof options.stabilization == "boolean") { + options.stabilization = { + enabled: options.stabilization + }; + } + util.deepExtend(this.options, options); + } + this.init(); + }, + writable: true, + configurable: true + }, + init: { + value: function init() { + var options; + if (this.options.model == "repulsion") { + options = this.options.repulsion; + this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } else if (this.options.model == "hierarchicalRepulsion") { + options = this.options.hierarchicalRepulsion; + this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); + this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); + } else { + // barnesHut + options = this.options.barnesHut; + this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } + + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; + }, + writable: true, + configurable: true + }, + startSimulation: { + value: function startSimulation() { + this.stabilized = false; + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } else { + this.runSimulation(); + } + }, + writable: true, + configurable: true + }, + runSimulation: { + value: function runSimulation() { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + }, + writable: true, + configurable: true + }, + simulationStep: { + value: function simulationStep() { + // check if the physics have settled + var startTime = Date.now(); + this.physicsTick(); + var physicsTime = Date.now() - startTime; + + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { + this.physicsTick(); + + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } + + if (this.stabilized === true) { + if (this.stabilizationIterations > 1) { + // trigger the "stabilized" event. + // The event is triggered on the next tick, to prevent the case that + // it is fired while initializing the Network, in which case you would not + // be able to catch it + var me = this; + var params = { + iterations: this.stabilizationIterations + }; + this.stabilizationIterations = 0; + this.startedStabilization = false; + setTimeout(function () { + me.body.emitter.emit("stabilized", params); + }, 0); + } else { + this.stabilizationIterations = 0; + } + this.body.emitter.emit("_stopRendering"); + } + }, + writable: true, + configurable: true + }, + physicsTick: { + + /** + * A single simulation step (or "tick") in the physics simulation + * + * @private + */ + value: function physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); + + // determine if the network has stabilzied + if (this.stabilized === true) { + this.revert(); + } else { + // this is here to ensure that there is no start event when the network is already stable. + if (this.startedStabilization == false) { + this.body.emitter.emit("startStabilizing"); + this.startedStabilization = true; + } + } - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - exports._getAllEdgesOverlappingWith = function (object) { - var overlappingEdges = []; - this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); - return overlappingEdges; - }; + this.stabilizationIterations++; + } + }, + writable: true, + configurable: true + }, + _updateCalculationNodes: { + + /** + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.body.nodes with the support nodes. + * + * @private + */ + value: function _updateCalculationNodes() { + this.physicsBody.calculationNodes = {}; + this.physicsBody.forces = {}; + this.physicsBody.calculationNodeIndices = []; + + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId]; + } + + // if support nodes are used, we have them here + var supportNodes = this.body.supportNodes; + for (var i = 0; i < this.body.supportNodeIndices.length; i++) { + var supportNodeId = this.body.supportNodeIndices[i]; + if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) { + this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + } else { + console.error("Support node detected that does not have an edge!"); + } + } - /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {null} - * @private - */ - exports._getEdgeAt = function(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes); + for (var i = 0; i < this.physicsBody.calculationNodeIndices.length; i++) { + var nodeId = this.physicsBody.calculationNodeIndices[i]; + this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; - if (overlappingEdges.length > 0) { - return this.edges[overlappingEdges[overlappingEdges.length - 1]]; - } - else { - return null; - } - }; + // forces can be reset because they are recalculated. Velocities have to persist. + if (this.physicsBody.velocities[nodeId] === undefined) { + this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; + } + } + // clean deleted nodes from the velocity vector + for (var nodeId in this.physicsBody.velocities) { + if (this.physicsBody.calculationNodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } + }, + writable: true, + configurable: true + }, + revert: { + value: function revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.physicsBody.calculationNodes; + var velocities = this.physicsBody.velocities; + + for (var i = 0; i < nodeIds.length; i++) { + var nodeId = nodeIds[i]; + if (nodes[nodeId] !== undefined) { + velocities[nodeId].x = this.previousStates[nodeId].vx; + velocities[nodeId].y = this.previousStates[nodeId].vy; + nodes[nodeId].x = this.previousStates[nodeId].x; + nodes[nodeId].y = this.previousStates[nodeId].y; + } else { + delete this.previousStates[nodeId]; + } + } + }, + writable: true, + configurable: true + }, + moveNodes: { + value: function moveNodes() { + var nodesPresent = false; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; + var stabilized = true; + var vminCorrected = this.options.minVelocity / Math.max(this.scale, 0.05); - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - exports._addToSelection = function(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } - else { - this.selectionObj.edges[obj.id] = obj; - } - }; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + var nodeVelocity = this._performStep(nodeId, maxVelocity); + // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized + stabilized = nodeVelocity < vminCorrected && stabilized === true; + nodesPresent = true; + } - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - exports._addToHover = function(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } - else { - this.hoverObj.edges[obj.id] = obj; - } - }; + if (nodesPresent == true) { + if (vminCorrected > 0.5 * this.options.maxVelocity) { + return false; + } else { + return stabilized; + } + } + return true; + }, + writable: true, + configurable: true + }, + _performStep: { + value: function _performStep(nodeId, maxVelocity) { + var node = this.physicsBody.calculationNodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; + + // store the state so we can revert + this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; + + if (!node.xFixed) { + var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force + var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration + velocities[nodeId].x += ax * timestep; // velocity + velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; + node.x += velocities[nodeId].x * timestep; // position + } else { + forces[nodeId].x = 0; + velocities[nodeId].x = 0; + } - /** - * Remove a single option from selection. - * - * @param {Object} obj - * @private - */ - exports._removeFromSelection = function(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } - else { - delete this.selectionObj.edges[obj.id]; - } - }; + if (!node.yFixed) { + var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force + var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration + velocities[nodeId].y += ay * timestep; // velocity + velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; + node.y += velocities[nodeId].y * timestep; // position + } else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; + } - /** - * Unselect all. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._unselectAll = function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); - } - } + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); + return totalVelocity; + }, + writable: true, + configurable: true + }, + calculateForces: { + value: function calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); + }, + writable: true, + configurable: true + }, + _freezeNodes: { - this.selectionObj = {nodes:{},edges:{}}; - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } - }; - /** - * Unselect all clusters. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._unselectClusters = function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - this.selectionObj.nodes[nodeId].unselect(); - this._removeFromSelection(this.selectionObj.nodes[nodeId]); - } - } - } - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } - }; - /** - * return the number of selected nodes - * - * @returns {number} - * @private - */ - exports._getSelectedNodeCount = function() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - return count; - }; - /** - * return the selected node - * - * @returns {number} - * @private - */ - exports._getSelectedNode = function() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return null; - }; - /** - * return the selected edge - * - * @returns {number} - * @private - */ - exports._getSelectedEdge = function() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } - } - return null; - }; + /** + * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization + * because only the supportnodes for the smoothCurves have to settle. + * + * @private + */ + value: function _freezeNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x != null && nodes[id].y != null) { + nodes[id].fixedData.x = nodes[id].xFixed; + nodes[id].fixedData.y = nodes[id].yFixed; + nodes[id].xFixed = true; + nodes[id].yFixed = true; + } + } + } + }, + writable: true, + configurable: true + }, + _restoreFrozenNodes: { + + /** + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * + * @private + */ + value: function _restoreFrozenNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].fixedData.x != null) { + nodes[id].xFixed = nodes[id].fixedData.x; + nodes[id].yFixed = nodes[id].fixedData.y; + } + } + } + }, + writable: true, + configurable: true + }, + stabilize: { + /** + * Find a stable position for all nodes + * @private + */ + value: function stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); + } + this.stabilizationSteps = 0; - /** - * return the number of selected edges - * - * @returns {number} - * @private - */ - exports._getSelectedEdgeCount = function() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; - }; + setTimeout(this._stabilizationBatch.bind(this), 0); + }, + writable: true, + configurable: true + }, + _stabilizationBatch: { + value: function _stabilizationBatch() { + var count = 0; + while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { + this.physicsTick(); + this.stabilizationSteps++; + count++; + } + + if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { + this.body.emitter.emit("stabilizationProgress", { steps: this.stabilizationSteps, total: this.options.stabilization.iterations }); + setTimeout(this._stabilizationBatch.bind(this), 0); + } else { + this._finalizeStabilization(); + } + }, + writable: true, + configurable: true + }, + _finalizeStabilization: { + value: function _finalizeStabilization() { + if (this.options.stabilization.zoomExtent == true) { + this.body.emitter.emit("zoomExtent", { duration: 0 }); + } + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); + } - /** - * return the number of selected objects. - * - * @returns {number} - * @private - */ - exports._getSelectedObjectCount = function() { - var count = 0; - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + }, + writable: true, + configurable: true } - } - return count; - }; + }); - /** - * Check if anything is selected - * - * @returns {boolean} - * @private - */ - exports._selectionIsEmpty = function() { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; - } - } - return true; - }; + return PhysicsEngine; + })(); + exports.PhysicsEngine = PhysicsEngine; + Object.defineProperty(exports, "__esModule", { + value: true + }); - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - exports._clusterInSelection = function() { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; - } - } - } - return false; - }; +/***/ }, +/* 67 */ +/***/ function(module, exports, __webpack_require__) { - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._selectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.select(); - this._addToSelection(edge); - } - }; + "use strict"; - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._hoverConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.hover = true; - this._addToHover(edge); - } - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node - * @private + * Created by Alex on 2/23/2015. */ - exports._unselectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.unselect(); - this._removeFromSelection(edge); + + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.barnesHutTree; + this.setOptions(options); } - }; + _prototypeProperties(BarnesHutSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + + + /** + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * + * @private + */ + value: function solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var nodeCount = nodeIndices.length; + + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + + // for debugging + this.barnesHutTree = barnesHutTree; + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } + } + } + }, + writable: true, + configurable: true + }, + _getForceContribution: { + + + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; + + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // BarnesHutSolver condition + // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.options.thetaInverted) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } else { + // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { + // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } + } + } + } + }, + writable: true, + configurable: true + }, + _formBarnesHutTree: { + + + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + value: function _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize + + + var minimumTreeSize = 0.00001; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), + centerY = 0.5 * (minY + maxY); + + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data: null }, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); + + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } + // make global + return barnesHutTree; + }, + writable: true, + configurable: true + }, + _updateBranchMass: { - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @param {Boolean} append - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._selectObject = function(object, append, doNotTrigger, highlightEdges, overrideSelectable) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - if (highlightEdges === undefined) { - highlightEdges = true; - } + /** + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private + */ + value: function _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; - if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { - this._unselectAll(true); - } + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; - // selectable allows the object to be selected. Override can be used if needed to bypass this. - if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) { - object.select(); - this._addToSelection(object); - if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { - this._selectConnectedEdges(object); - } - } - // do not select the object if selectable is false, only add it to selection to allow drag to work - else if (object.selected == false) { - this._addToSelection(object); - doNotTrigger = true; - } - else { - object.unselect(); - this._removeFromSelection(object); - } + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } - }; + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + }, + writable: true, + configurable: true + }, + _placeInTree: { + + + /** + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private + */ + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } else { + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); + } else { + // in SE + this._placeInRegion(parentBranch, node, "SE"); + } + } + }, + writable: true, + configurable: true + }, + _placeInRegion: { + + + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + value: function _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: + // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: + // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); + } else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); + } + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; + } + }, + writable: true, + configurable: true + }, + _splitBranch: { + + + /** + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch + * @private + */ + value: function _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; + } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } + }, + writable: true, + configurable: true + }, + _insertRegion: { + + + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + value: function _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - exports._blurObject = function(object) { - if (object.hover == true) { - object.hover = false; - this.emit("blurNode",{node:object.id}); - } - }; + parentBranch.children[region] = { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: { data: null }, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; + }, + writable: true, + configurable: true + }, + _debug: { - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - exports._hoverObject = function(object) { - if (object.hover == false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.emit("hoverNode",{node:object.id}); - } - } - if (object instanceof Node) { - this._hoverConnectedEdges(object); - } - }; - /** - * handles the selection part of the touch, only for navigation controls elements; - * Touch is triggered before tap, also before hold. Hold triggers after a while. - * This is the most responsive solution - * - * @param {Object} pointer - * @private - */ - exports._handleTouch = function(pointer) { - }; + //--------------------------- DEBUGGING BELOW ---------------------------// - /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private - */ - exports._handleTap = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node, false); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge, false); - } - else { - this._unselectAll(); - } - } - var properties = this.getSelection(); - properties['pointer'] = { - DOM: {x: pointer.x, y: pointer.y}, - canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} - } - this.emit("click", properties); - this._requestRedraw(); - }; + /** + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private + */ + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { + ctx.lineWidth = 1; - /** - * handles the selection part of the double tap and opens a cluster if needed - * - * @param {Object} pointer - * @private - */ - exports._handleDoubleTap = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; - this.openCluster(node); - } - var properties = this.getSelection(); - properties['pointer'] = { - DOM: {x: pointer.x, y: pointer.y}, - canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} - } - this.emit("doubleClick", properties); - }; + this._drawBranch(this.barnesHutTree.root, ctx, color); + } + }, + writable: true, + configurable: true + }, + _drawBranch: { - /** - * Handle the onHold selection part - * - * @param pointer - * @private - */ - exports._handleOnHold = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,true); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,true); - } - } - this._requestRedraw(); - }; + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); - /** - * handle the onRelease event. These functions are here for the navigation controls module - * and data manipulation module. - * - * @private - */ - exports._handleOnRelease = function(pointer) { - this._manipulationReleaseOverload(pointer); - this._navigationReleaseOverload(pointer); - }; + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); - exports._manipulationReleaseOverload = function (pointer) {}; - exports._navigationReleaseOverload = function (pointer) {}; + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); - /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection - */ - exports.getSelection = function() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return {nodes:nodeIds, edges:edgeIds}; - }; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); - /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. - */ - exports.getSelectedNodes = function() { - var idArray = []; - if (this.constants.selectable == true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ + }, + writable: true, + configurable: true } - } - return idArray - }; + }); - /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. - */ - exports.getSelectedEdges = function() { - var idArray = []; - if (this.constants.selectable == true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } - } - } - return idArray; - }; + return BarnesHutSolver; + })(); + exports.BarnesHutSolver = BarnesHutSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - /** - * select zero or more nodes DEPRICATED - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - exports.setSelection = function() { - console.log("setSelection is deprecated. Please use selectNodes instead.") - }; +/***/ }, +/* 68 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] + * Created by Alex on 2/23/2015. */ - exports.selectNodes = function(selection, highlightEdges) { - var i, iMax, id; - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; + var RepulsionSolver = (function () { + function RepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, RepulsionSolver); - // first unselect any selected node - this._unselectAll(true); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + _prototypeProperties(RepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // approximation constants + var a = -2 / 3 / nodeDistance; + var b = 4 / 3; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (var i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (var j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); + if (distance < 2 * nodeDistance) { + if (distance < 0.5 * nodeDistance) { + repulsingForce = 1; + } else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) + } + repulsingForce = repulsingForce / distance; + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } + }, + writable: true, + configurable: true } - this._selectObject(node,true,true,highlightEdges,true); - } - this.redraw(); - }; + }); + return RepulsionSolver; + })(); - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - exports.selectEdges = function(selection) { - var i, iMax, id; + exports.RepulsionSolver = RepulsionSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; +/***/ }, +/* 69 */ +/***/ function(module, exports, __webpack_require__) { - // first unselect any selected node - this._unselectAll(true); + "use strict"; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var edge = this.edges[id]; - if (!edge) { - throw new RangeError('Edge with id "' + id + '" not found'); - } - this._selectObject(edge,true,true,false,true); - } - this.redraw(); - }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Validate the selection: remove ids of nodes which no longer exist - * @private + * Created by Alex on 2/23/2015. */ - exports._updateSelection = function () { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; - } - } + + var HierarchicalRepulsionSolver = (function () { + function HierarchicalRepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalRepulsionSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; - } + + _prototypeProperties(HierarchicalRepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; + + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + // nodes only affect nodes on their level + if (node1.level == node2.level) { + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); + } else { + repulsingForce = 0; + } + // normalize force with + if (distance == 0) { + distance = 0.01; + } else { + repulsingForce = repulsingForce / distance; + } + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } + }, + writable: true, + configurable: true } - } - }; + }); + return HierarchicalRepulsionSolver; + })(); + + exports.HierarchicalRepulsionSolver = HierarchicalRepulsionSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ }, -/* 67 */ +/* 70 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); - var Node = __webpack_require__(56); - var Edge = __webpack_require__(57); - var Hammer = __webpack_require__(19); + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * clears the toolbar div element of children - * - * @private + * Created by Alex on 2/23/2015. */ - exports._clearManipulatorBar = function() { - this._recursiveDOMDelete(this.manipulationDiv); - this.manipulationDOM = {}; - this._cleanManipulatorHammers(); + var SpringSolver = (function () { + function SpringSolver(body, physicsBody, options) { + _classCallCheck(this, SpringSolver); - this._manipulationReleaseOverload = function () {}; - delete this.sectors['support']['nodes']['targetNode']; - delete this.sectors['support']['nodes']['targetViaNode']; - this.controlNodesActive = false; - this.freezeSimulation(false); - }; + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + _prototypeProperties(SpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + value: function solve() { + var edgeLength, edge, edgeId; + var edges = this.body.edges; + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected === true) { + // only calculate forces if nodes are in the same sector + if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { + edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; + if (edge.via != null) { + var node1 = edge.to; + var node2 = edge.via; + var node3 = edge.from; + + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } else { + this._calculateSpringForce(edge.from, edge.to, edgeLength); + } + } + } + } + } + }, + writable: true, + configurable: true + }, + _calculateSpringForce: { + + + /** + * This is the code actually performing the calculation for the function above. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private + */ + value: function _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; + + dx = node1.x - node2.x; + dy = node1.y - node2.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; - exports._cleanManipulatorHammers = function() { - // clean hammer bindings - if (this.manipulationHammers.length != 0) { - for (var i = 0; i < this.manipulationHammers.length; i++) { - this.manipulationHammers[i].dispose(); - } - this.manipulationHammers = []; - } - }; + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - /** - * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore - * these functions to their original functionality, we saved them in this.cachedFunctions. - * This function restores these functions to their original function. - * - * @private - */ - exports._restoreOverloadedFunctions = function() { - for (var functionName in this.cachedFunctions) { - if (this.cachedFunctions.hasOwnProperty(functionName)) { - this[functionName] = this.cachedFunctions[functionName]; - delete this.cachedFunctions[functionName]; + fx = dx * springForce; + fy = dy * springForce; + + this.physicsBody.forces[node1.id].x += fx; + this.physicsBody.forces[node1.id].y += fy; + this.physicsBody.forces[node2.id].x -= fx; + this.physicsBody.forces[node2.id].y -= fy; + }, + writable: true, + configurable: true } - } - }; + }); - /** - * Enable or disable edit-mode. - * - * @private - */ - exports._toggleEditMode = function() { - this.editMode = !this.editMode; - var toolbar = this.manipulationDiv; - var closeDiv = this.closeDiv; - var editModeDiv = this.editModeDiv; - if (this.editMode == true) { - toolbar.style.display="block"; - closeDiv.style.display="block"; - editModeDiv.style.display="none"; - this._bindHammerToDiv(closeDiv,'_toggleEditMode'); - } - else { - toolbar.style.display="none"; - closeDiv.style.display="none"; - editModeDiv.style.display="block"; - } - this._createManipulatorBar() - }; + return SpringSolver; + })(); + + exports.SpringSolver = SpringSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); + +/***/ }, +/* 71 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private + * Created by Alex on 2/25/2015. */ - exports._createManipulatorBar = function() { - // remove bound functions - if (this.boundFunction) { - this.off('select', this.boundFunction); - } - this._cleanManipulatorHammers(); - - var locale = this.constants.locales[this.constants.locale]; + var HierarchicalSpringSolver = (function () { + function HierarchicalSpringSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalSpringSolver); - if (this.edgeBeingEdited !== undefined) { - this.edgeBeingEdited._disableControlNodes(); - this.edgeBeingEdited = undefined; - this.selectedControlNode = null; - this.controlNodesActive = false; - this._redraw(); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); } - // restore overloaded functions - this._restoreOverloadedFunctions(); + _prototypeProperties(HierarchicalSpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + value: function solve() { + var edgeLength, edge, edgeId; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + // initialize the spring force counters + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].springFx = 0; + forces[nodeId].springFy = 0; + } + + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected === true) { + edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; + + dx = edge.from.x - edge.to.x; + dy = edge.from.y - edge.to.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + if (edge.to.level != edge.from.level) { + forces[edge.toId].springFx -= fx; + forces[edge.toId].springFy -= fy; + forces[edge.fromId].springFx += fx; + forces[edge.fromId].springFy += fy; + } else { + var factor = 0.5; + forces[edge.toId].x -= factor * fx; + forces[edge.toId].y -= factor * fy; + forces[edge.fromId].x += factor * fx; + forces[edge.fromId].y += factor * fy; + } + } + } + } - // resume calculation - this.freezeSimulation(false); + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); + springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false; - this.manipulationDOM = {}; + forces[nodeId].x += springFx; + forces[nodeId].y += springFy; + } - if (this.editMode == true) { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + totalFx += forces[nodeId].x; + totalFy += forces[nodeId].y; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; + + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].x -= correctionFx; + forces[nodeId].y -= correctionFy; + } + }, + writable: true, + configurable: true } + }); - this.manipulationDOM['addNodeSpan'] = document.createElement('div'); - this.manipulationDOM['addNodeSpan'].className = 'network-manipulationUI add'; + return HierarchicalSpringSolver; + })(); - this.manipulationDOM['addNodeLabelSpan'] = document.createElement('div'); - this.manipulationDOM['addNodeLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['addNodeLabelSpan'].innerHTML = locale['addNode']; - this.manipulationDOM['addNodeSpan'].appendChild(this.manipulationDOM['addNodeLabelSpan']); + exports.HierarchicalSpringSolver = HierarchicalSpringSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; +/***/ }, +/* 72 */ +/***/ function(module, exports, __webpack_require__) { - this.manipulationDOM['addEdgeSpan'] = document.createElement('div'); - this.manipulationDOM['addEdgeSpan'].className = 'network-manipulationUI connect'; - this.manipulationDOM['addEdgeLabelSpan'] = document.createElement('div'); - this.manipulationDOM['addEdgeLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['addEdgeLabelSpan'].innerHTML = locale['addEdge']; - this.manipulationDOM['addEdgeSpan'].appendChild(this.manipulationDOM['addEdgeLabelSpan']); + "use strict"; - this.manipulationDiv.appendChild(this.manipulationDOM['addNodeSpan']); - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']); - this.manipulationDiv.appendChild(this.manipulationDOM['addEdgeSpan']); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - this.manipulationDOM['seperatorLineDiv2'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine'; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.manipulationDOM['editNodeSpan'] = document.createElement('div'); - this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit'; - this.manipulationDOM['editNodeLabelSpan'] = document.createElement('div'); - this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode']; - this.manipulationDOM['editNodeSpan'].appendChild(this.manipulationDOM['editNodeLabelSpan']); + /** + * Created by Alex on 2/23/2015. + */ - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv2']); - this.manipulationDiv.appendChild(this.manipulationDOM['editNodeSpan']); - } - else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - this.manipulationDOM['seperatorLineDiv3'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine'; + var CentralGravitySolver = (function () { + function CentralGravitySolver(body, physicsBody, options) { + _classCallCheck(this, CentralGravitySolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - this.manipulationDOM['editEdgeSpan'] = document.createElement('div'); - this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit'; - this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('div'); - this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge']; - this.manipulationDOM['editEdgeSpan'].appendChild(this.manipulationDOM['editEdgeLabelSpan']); + _prototypeProperties(CentralGravitySolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + value: function solve() { + var dx, dy, distance, node, i; + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + var gravity = this.options.centralGravity; + var gravityForce = 0; + + for (i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + node = nodes[nodeId]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv3']); - this.manipulationDiv.appendChild(this.manipulationDOM['editEdgeSpan']); + gravityForce = distance == 0 ? 0 : gravity / distance; + forces[nodeId].x = dx * gravityForce; + forces[nodeId].y = dy * gravityForce; + } + }, + writable: true, + configurable: true } - if (this._selectionIsEmpty() == false) { - this.manipulationDOM['seperatorLineDiv4'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv4'].className = 'network-seperatorLine'; + }); - this.manipulationDOM['deleteSpan'] = document.createElement('div'); - this.manipulationDOM['deleteSpan'].className = 'network-manipulationUI delete'; - this.manipulationDOM['deleteLabelSpan'] = document.createElement('div'); - this.manipulationDOM['deleteLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['deleteLabelSpan'].innerHTML = locale['del']; - this.manipulationDOM['deleteSpan'].appendChild(this.manipulationDOM['deleteLabelSpan']); + return CentralGravitySolver; + })(); - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv4']); - this.manipulationDiv.appendChild(this.manipulationDOM['deleteSpan']); - } + exports.CentralGravitySolver = CentralGravitySolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - // bind the icons - this._bindHammerToDiv(this.manipulationDOM['addNodeSpan'],'_createAddNodeToolbar'); - this._bindHammerToDiv(this.manipulationDOM['addEdgeSpan'],'_createAddEdgeToolbar'); - this._bindHammerToDiv(this.closeDiv,'_toggleEditMode'); +/***/ }, +/* 73 */ +/***/ function(module, exports, __webpack_require__) { - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - this._bindHammerToDiv(this.manipulationDOM['editNodeSpan'],'_editNode'); - } - else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - this._bindHammerToDiv(this.manipulationDOM['editEdgeSpan'],'_createEditEdgeToolbar'); - } - if (this._selectionIsEmpty() == false) { - this._bindHammerToDiv(this.manipulationDOM['deleteSpan'],'_deleteSelected'); - } + "use strict"; - var me = this; - this.boundFunction = me._createManipulatorBar; - this.on('select', this.boundFunction); - } - else { - while (this.editModeDiv.hasChildNodes()) { - this.editModeDiv.removeChild(this.editModeDiv.firstChild); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 24-Feb-15. + */ - this.manipulationDOM['editModeSpan'] = document.createElement('div'); - this.manipulationDOM['editModeSpan'].className = 'network-manipulationUI edit editmode'; - this.manipulationDOM['editModeLabelSpan'] = document.createElement('div'); - this.manipulationDOM['editModeLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['editModeLabelSpan'].innerHTML = locale['edit']; - this.manipulationDOM['editModeSpan'].appendChild(this.manipulationDOM['editModeLabelSpan']); + var util = __webpack_require__(1); - this.editModeDiv.appendChild(this.manipulationDOM['editModeSpan']); + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); - this._bindHammerToDiv(this.manipulationDOM['editModeSpan'],'_toggleEditMode'); + this.body = body; + this.clusteredNodes = {}; } - }; + _prototypeProperties(ClusterEngine, null, { + clusterByConnectionCount: { - exports._bindHammerToDiv = function(domElement, funct) { - var hammer = Hammer(domElement, {prevent_default: true}); - hammer.on('touch', this[funct].bind(this)); - this.manipulationHammers.push(hammer); - } + /** + * + * @param hubsize + * @param options + */ + value: function clusterByConnectionCount(hubsize, options) { + if (hubsize === undefined) { + hubsize = this._getHubSize(); + } else if (tyepof(hubsize) == "object") { + options = this._checkOptions(hubsize); + hubsize = this._getHubSize(); + } - /** - * Create the toolbar for adding Nodes - * - * @private - */ - exports._createAddNodeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - if (this.boundFunction) { - this.off('select', this.boundFunction); - } + var nodesToCluster = []; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length >= hubsize) { + nodesToCluster.push(node.id); + } + } - var locale = this.constants.locales[this.constants.locale]; + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node, options, {}, {}, true); + } + this.body.emitter.emit("_dataChanged"); + }, + writable: true, + configurable: true + }, + clusterByNodeData: { - this.manipulationDOM = {}; - this.manipulationDOM['backSpan'] = document.createElement('div'); - this.manipulationDOM['backSpan'].className = 'network-manipulationUI back'; - this.manipulationDOM['backLabelSpan'] = document.createElement('div'); - this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['backLabelSpan'].innerHTML = locale['back']; - this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']); - - this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; - - this.manipulationDOM['descriptionSpan'] = document.createElement('div'); - this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none'; - this.manipulationDOM['descriptionLabelSpan'] = document.createElement('div'); - this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['addDescription']; - this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']); - - this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']); - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']); - this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']); - // bind the icon - this._bindHammerToDiv(this.manipulationDOM['backSpan'],'_createManipulatorBar'); + /** + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param doNotUpdateCalculationNodes + */ + value: function clusterByNodeData() { + var options = arguments[0] === undefined ? {} : arguments[0]; + var doNotUpdateCalculationNodes = arguments[1] === undefined ? false : arguments[1]; + if (options.joinCondition === undefined) { + throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + } - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - var me = this; - this.boundFunction = me._addNode; - this.on('select', this.boundFunction); - }; + // check if the options object is fine, append if needed + options = this._checkOptions(options); + var childNodesObj = {}; + var childEdgesObj = {}; - /** - * create the toolbar to connect nodes - * - * @private - */ - exports._createAddEdgeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - this._unselectAll(true); - this.freezeSimulation(true); + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + } - if (this.boundFunction) { - this.off('select', this.boundFunction); - } + this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); + }, + writable: true, + configurable: true + }, + clusterOutliers: { + + + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param doNotUpdateCalculationNodes + */ + value: function clusterOutliers(options, doNotUpdateCalculationNodes) { + options = this._checkOptions(options); + var clusters = []; + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var childNodesObj = {}; + var childEdgesObj = {}; + var nodeId = this.body.nodeIndices[i]; + if (this.body.nodes[nodeId].edges.length == 1) { + var edge = this.body.nodes[nodeId].edges[0]; + var childNodeId = this._getConnectedId(edge, nodeId); + if (childNodeId != nodeId) { + if (options.joinCondition === undefined) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + clonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); + } + } + } - var locale = this.constants.locales[this.constants.locale]; + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, true); + } - this._unselectAll(); - this.forceAppendSelection = false; - this.blockConnectingEdgeSelection = true; + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + clusterByConnection: { + + /** + * + * @param nodeId + * @param options + * @param doNotUpdateCalculationNodes + */ + value: function clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) { + // kill conditions + if (nodeId === undefined) { + throw new Error("No nodeId supplied to clusterByConnection!"); + } + if (this.body.nodes[nodeId] === undefined) { + throw new Error("The nodeId given to clusterByConnection does not exist!"); + } + + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) { + options.clusterNodeProperties.x = node.x;options.clusterNodeProperties.allowedToMoveX = !node.xFixed; + } + if (options.clusterNodeProperties.y === undefined) { + options.clusterNodeProperties.y = node.y;options.clusterNodeProperties.allowedToMoveY = !node.yFixed; + } + + var childNodesObj = {}; + var childEdgesObj = {}; + var parentNodeId = node.id; + var parentClonedOptions = this._cloneOptions(parentNodeId); + childNodesObj[parentNodeId] = node; + + // collect the nodes that will be in the cluster + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + var childNodeId = this._getConnectedId(edge, parentNodeId); + + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + } else { + childEdgesObj[edge.id] = edge; + } + } - this.manipulationDOM = {}; - this.manipulationDOM['backSpan'] = document.createElement('div'); - this.manipulationDOM['backSpan'].className = 'network-manipulationUI back'; - this.manipulationDOM['backLabelSpan'] = document.createElement('div'); - this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['backLabelSpan'].innerHTML = locale['back']; - this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']); - - this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; - - this.manipulationDOM['descriptionSpan'] = document.createElement('div'); - this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none'; - this.manipulationDOM['descriptionLabelSpan'] = document.createElement('div'); - this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['edgeDescription']; - this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']); - - this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']); - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']); - this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']); + this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); + }, + writable: true, + configurable: true + }, + _cloneOptions: { + + + /** + * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes. + * @param objId + * @param type + * @returns {{}} + * @private + */ + value: function _cloneOptions(objId, type) { + var clonedOptions = {}; + if (type === undefined || type == "node") { + util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); + util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); + clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + } else { + util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + } + return clonedOptions; + }, + writable: true, + configurable: true + }, + _createClusterEdges: { + + + /** + * This function creates the edges that will be attached to the cluster. + * + * @param childNodesObj + * @param childEdgesObj + * @param newEdges + * @param options + * @private + */ + value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { + var edge, childNodeId, childNode; + + var childKeys = Object.keys(childNodesObj); + for (var i = 0; i < childKeys.length; i++) { + childNodeId = childKeys[i]; + childNode = childNodesObj[childNodeId]; + + // mark all edges for removal from global and construct new edges from the cluster to others + for (var j = 0; j < childNode.edges.length; j++) { + edge = childNode.edges[j]; + childEdgesObj[edge.id] = edge; + + var otherNodeId = edge.toId; + var otherOnTo = true; + if (edge.toId != childNodeId) { + otherNodeId = edge.toId; + otherOnTo = true; + } else if (edge.fromId != childNodeId) { + otherNodeId = edge.fromId; + otherOnTo = false; + } - // bind the icon - this._bindHammerToDiv(this.manipulationDOM['backSpan'],'_createManipulatorBar'); + if (childNodesObj[otherNodeId] === undefined) { + var clonedOptions = this._cloneOptions(edge.id, "edge"); + util.deepExtend(clonedOptions, options.clusterEdgeProperties); + if (otherOnTo === true) { + clonedOptions.from = options.clusterNodeProperties.id; + clonedOptions.to = otherNodeId; + } else { + clonedOptions.from = otherNodeId; + clonedOptions.to = options.clusterNodeProperties.id; + } + clonedOptions.id = "clusterEdge:" + util.randomUUID(); + newEdges.push(this.body.functions.createEdge(clonedOptions)); + } + } + } + }, + writable: true, + configurable: true + }, + _checkOptions: { - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - var me = this; - this.boundFunction = me._handleConnect; - this.on('select', this.boundFunction); - // temporarily overload functions - this.cachedFunctions["_handleTouch"] = this._handleTouch; - this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload; - this.cachedFunctions["_handleDragStart"] = this._handleDragStart; - this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd; - this.cachedFunctions["_handleOnHold"] = this._handleOnHold; - this._handleTouch = this._handleConnect; - this._manipulationReleaseOverload = function () {}; - this._handleOnHold = function () {}; - this._handleDragStart = function () {}; - this._handleDragEnd = this._finishConnect; + /** + * This function checks the options that can be supplied to the different cluster functions + * for certain fields and inserts defaults if needed + * @param options + * @returns {*} + * @private + */ + value: function _checkOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + if (options.clusterEdgeProperties === undefined) { + options.clusterEdgeProperties = {}; + } + if (options.clusterNodeProperties === undefined) { + options.clusterNodeProperties = {}; + } - // redraw to show the unselect - this._redraw(); - }; + return options; + }, + writable: true, + configurable: true + }, + _cluster: { + + /** + * + * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node + * @param {Object} childEdgesObj | object with edge objects, id as keys + * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} + * @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up + * @private + */ + value: function _cluster(childNodesObj, childEdgesObj, options) { + var doNotUpdateCalculationNodes = arguments[3] === undefined ? false : arguments[3]; + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length == 0) { + return; + } + + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) { + options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + } + var clusterId = options.clusterNodeProperties.id; + + // create the new edges that will connect to the cluster + var newEdges = []; + this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + + // construct the clusterNodeProperties + var clusterNodeProperties = options.clusterNodeProperties; + if (options.processProperties !== undefined) { + // get the childNode options + var childNodesOptions = []; + for (var nodeId in childNodesObj) { + var clonedOptions = this._cloneOptions(nodeId); + childNodesOptions.push(clonedOptions); + } - /** - * create the toolbar to edit edges - * - * @private - */ - exports._createEditEdgeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - this.controlNodesActive = true; + // get clusterproperties based on childNodes + var childEdgesOptions = []; + for (var edgeId in childEdgesObj) { + var clonedOptions = this._cloneOptions(edgeId, "edge"); + childEdgesOptions.push(clonedOptions); + } + + clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); + if (!clusterNodeProperties) { + throw new Error("The processClusterProperties function does not return properties!"); + } + } + if (clusterNodeProperties.label === undefined) { + clusterNodeProperties.label = "cluster"; + } + + + // give the clusterNode a postion if it does not have one. + var pos = undefined; + if (clusterNodeProperties.x === undefined) { + pos = this._getClusterPosition(childNodesObj); + clusterNodeProperties.x = pos.x; + clusterNodeProperties.allowedToMoveX = true; + } + if (clusterNodeProperties.x === undefined) { + if (pos === undefined) { + pos = this._getClusterPosition(childNodesObj); + } + clusterNodeProperties.y = pos.y; + clusterNodeProperties.allowedToMoveY = true; + } - if (this.boundFunction) { - this.off('select', this.boundFunction); - } - this.edgeBeingEdited = this._getSelectedEdge(); - this.edgeBeingEdited._enableControlNodes(); + // force the ID to remain the same + clusterNodeProperties.id = clusterId; - var locale = this.constants.locales[this.constants.locale]; - this.manipulationDOM = {}; - this.manipulationDOM['backSpan'] = document.createElement('div'); - this.manipulationDOM['backSpan'].className = 'network-manipulationUI back'; - this.manipulationDOM['backLabelSpan'] = document.createElement('div'); - this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['backLabelSpan'].innerHTML = locale['back']; - this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']); - - this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine'; - - this.manipulationDOM['descriptionSpan'] = document.createElement('div'); - this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none'; - this.manipulationDOM['descriptionLabelSpan'] = document.createElement('div'); - this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel'; - this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['editEdgeDescription']; - this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']); - - this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']); - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']); - this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']); + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; - // bind the icon - this._bindHammerToDiv(this.manipulationDOM['backSpan'],'_createManipulatorBar'); - // temporarily overload functions - this.cachedFunctions["_handleTouch"] = this._handleTouch; - this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload; - this.cachedFunctions["_handleTap"] = this._handleTap; - this.cachedFunctions["_handleDragStart"] = this._handleDragStart; - this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; - this._handleTouch = this._selectControlNode; - this._handleTap = function () {}; - this._handleOnDrag = this._controlNodeDrag; - this._handleDragStart = function () {} - this._manipulationReleaseOverload = this._releaseControlNode; + // delete contained edges from global + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + if (this.body.edges[edgeId].via !== null) { + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = null; + delete this.body.supportNodes[viaId]; + } + } + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } + } + } - // redraw to show the unselect - this._redraw(); - }; + // remove contained nodes from global + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; + delete this.body.nodes[nodeId]; + } + } - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - exports._selectControlNode = function(pointer) { - this.edgeBeingEdited.controlNodes.from.unselect(); - this.edgeBeingEdited.controlNodes.to.unselect(); - this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); - if (this.selectedControlNode !== null) { - this.selectedControlNode.select(); - this.freezeSimulation(true); - } - this._redraw(); - }; + // finally put the cluster node into global + this.body.nodes[clusterNodeProperties.id] = clusterNode; - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - exports._controlNodeDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { - this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); - this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); - } - this._redraw(); - }; + // push new edges to global + for (var i = 0; i < newEdges.length; i++) { + this.body.edges[newEdges[i].id] = newEdges[i]; + this.body.edges[newEdges[i].id].connect(); + } - /** - * - * @param pointer - * @private - */ - exports._releaseControlNode = function(pointer) { - var newNode = this._getNodeAt(pointer); - if (newNode !== null) { - if (this.edgeBeingEdited.controlNodes.from.selected == true) { - this.edgeBeingEdited._restoreControlNodes(); - this._editEdge(newNode.id, this.edgeBeingEdited.to.id); - this.edgeBeingEdited.controlNodes.from.unselect(); - } - if (this.edgeBeingEdited.controlNodes.to.selected == true) { - this.edgeBeingEdited._restoreControlNodes(); - this._editEdge(this.edgeBeingEdited.from.id, newNode.id); - this.edgeBeingEdited.controlNodes.to.unselect(); - } - } - else { - this.edgeBeingEdited._restoreControlNodes(); - } - this.freezeSimulation(false); - this._redraw(); - }; - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - exports._handleConnect = function(pointer) { - if (this._getSelectedNodeCount() == 0) { - var node = this._getNodeAt(pointer); + // create bezier nodes for smooth curves if needed + this.body.emitter.emit("_newEdgesCreated"); - if (node != null) { - if (node.clusterSize > 1) { - alert(this.constants.locales[this.constants.locale]['createEdgeError']) - } - else { - this._selectObject(node,false); - var supportNodes = this.sectors['support']['nodes']; - // create a node the temporary line can look at - supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); - var targetNode = supportNodes['targetNode']; - targetNode.x = node.x; - targetNode.y = node.y; + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; - // create a temporary edge - this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants); - var connectionEdge = this.edges['connectionEdge']; - connectionEdge.from = node; - connectionEdge.connected = true; - connectionEdge.options.smoothCurves = {enabled: true, - dynamic: false, - type: "continuous", - roundness: 0.5 - }; - connectionEdge.selected = true; - connectionEdge.to = targetNode; - this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; - var me = this; - this._handleOnDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - var connectionEdge = me.edges['connectionEdge']; - connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x); - connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y); - me._redraw(); - }; + // wrap up + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + isCluster: { - this.moving = true; - this.start(); - } - } - } - }; - exports._finishConnect = function(event) { - if (this._getSelectedNodeCount() == 1) { - var pointer = this._getPointer(event.gesture.center); - // restore the drag function - this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; - delete this.cachedFunctions["_handleOnDrag"]; + /** + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ + value: function isCluster(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster; + } else { + console.log("Node does not exist."); + return false; + } + }, + writable: true, + configurable: true + }, + _getClusterPosition: { + + /** + * get the position of the cluster node based on what's inside + * @param {object} childNodesObj | object with node objects, id as keys + * @returns {{x: number, y: number}} + * @private + */ + value: function _getClusterPosition(childNodesObj) { + var childKeys = Object.keys(childNodesObj); + var minX = childNodesObj[childKeys[0]].x; + var maxX = childNodesObj[childKeys[0]].x; + var minY = childNodesObj[childKeys[0]].y; + var maxY = childNodesObj[childKeys[0]].y; + var node; + for (var i = 0; i < childKeys.lenght; i++) { + node = childNodesObj[childKeys[0]]; + minX = node.x < minX ? node.x : minX; + maxX = node.x > maxX ? node.x : maxX; + minY = node.y < minY ? node.y : minY; + maxY = node.y > maxY ? node.y : maxY; + } + return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; + }, + writable: true, + configurable: true + }, + openCluster: { - // remember the edge id - var connectFromId = this.edges['connectionEdge'].fromId; - // remove the temporary nodes and edge - delete this.edges['connectionEdge']; - delete this.sectors['support']['nodes']['targetNode']; - delete this.sectors['support']['nodes']['targetViaNode']; + /** + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true + */ + value: function openCluster(clusterNodeId, doNotUpdateCalculationNodes) { + // kill conditions + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); + } + if (this.body.nodes[clusterNodeId] === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); + } + if (this.body.nodes[clusterNodeId].containedNodes === undefined) { + console.log("The node:" + clusterNodeId + " is not a cluster.");return; + }; - var node = this._getNodeAt(pointer); - if (node != null) { - if (node.clusterSize > 1) { - alert(this.constants.locales[this.constants.locale]["createEdgeError"]) - } - else { - this._createEdge(connectFromId,node.id); - this._createManipulatorBar(); - } - } - this._unselectAll(); - } - }; + var node = this.body.nodes[clusterNodeId]; + var containedNodes = node.containedNodes; + var containedEdges = node.containedEdges; + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId] = containedNodes[nodeId]; + // inherit position + this.body.nodes[nodeId].x = node.x; + this.body.nodes[nodeId].y = node.y; - /** - * Adds a node on the specified location - */ - exports._addNode = function() { - if (this._selectionIsEmpty() && this.editMode == true) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; - if (this.triggerFunctions.add) { - if (this.triggerFunctions.add.length == 2) { - var me = this; - this.triggerFunctions.add(defaultData, function(finalizedData) { - me.nodesData.add(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for add does not support two arguments (data,callback)'); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - else { - this.nodesData.add(defaultData); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - }; + // inherit speed + this.body.nodes[nodeId].vx = node.vx; + this.body.nodes[nodeId].vy = node.vy; + delete this.clusteredNodes[nodeId]; + } + } - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._createEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.connect) { - if (this.triggerFunctions.connect.length == 2) { - var me = this; - this.triggerFunctions.connect(defaultData, function(finalizedData) { - me.edgesData.add(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for connect does not support two arguments (data,callback)'); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.add(defaultData); - this.moving = true; - this.start(); - } - } - }; + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId] = containedEdges[edgeId]; + this.body.edges[edgeId].connect(); + var edge = this.body.edges[edgeId]; + if (edge.connected === false) { + if (this.clusteredNodes[edge.fromId] !== undefined) { + this._connectEdge(edge, edge.fromId, true); + } + if (this.clusteredNodes[edge.toId] !== undefined) { + this._connectEdge(edge, edge.toId, false); + } + } + } + } - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._editEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.editEdge) { - if (this.triggerFunctions.editEdge.length == 2) { - var me = this; - this.triggerFunctions.editEdge(defaultData, function(finalizedData) { - me.edgesData.update(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.update(defaultData); - this.moving = true; - this.start(); - } - } - }; + this.body.emitter.emit("_newEdgesCreated", containedEdges); - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * - * @private - */ - exports._editNode = function() { - if (this.triggerFunctions.edit && this.editMode == true) { - var node = this._getSelectedNode(); - var data = {id:node.id, - label: node.label, - group: node.options.group, - shape: node.options.shape, - color: { - background:node.options.color.background, - border:node.options.color.border, - highlight: { - background:node.options.color.highlight.background, - border:node.options.color.highlight.border + + var edgeIds = []; + for (var i = 0; i < node.edges.length; i++) { + edgeIds.push(node.edges[i].id); } - }}; - if (this.triggerFunctions.edit.length == 2) { - var me = this; - this.triggerFunctions.edit(data, function (finalizedData) { - me.nodesData.update(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - } - } - else { - throw new Error('No edit function has been bound to this button'); - } - }; + // remove edges in clusterNode + for (var i = 0; i < edgeIds.length; i++) { + var edge = this.body.edges[edgeIds[i]]; + // if the edge should have been connected to a contained node + if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) { + // the node in the from array was contained in the cluster + if (this.body.nodes[edge.fromArray[0].id] !== undefined) { + this._connectEdge(edge, edge.fromArray[0].id, true); + } + } else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) { + // the node in the to array was contained in the cluster + if (this.body.nodes[edge.toArray[0].id] !== undefined) { + this._connectEdge(edge, edge.toArray[0].id, false); + } + } else { + var edgeId = edgeIds[i]; + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = null; + delete this.body.supportNodes[viaId]; + } + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } + } + + // remove clusterNode + delete this.body.nodes[clusterNodeId]; + + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + _connectEdge: { + + + + /** + * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to + * is currently residing in cluster B + * @param edge + * @param nodeId + * @param from + * @private + */ + value: function _connectEdge(edge, nodeId, from) { + var clusterStack = this._getClusterStack(nodeId); + if (from == true) { + edge.from = clusterStack[clusterStack.length - 1]; + edge.fromId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.fromArray = clusterStack; + } else { + edge.to = clusterStack[clusterStack.length - 1]; + edge.toId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.toArray = clusterStack; + } + edge.connect(); + }, + writable: true, + configurable: true + }, + _getClusterStack: { + + /** + * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node + * @param nodeId + * @returns {Array} + * @private + */ + value: function _getClusterStack(nodeId) { + var stack = []; + var max = 100; + var counter = 0; + + while (this.clusteredNodes[nodeId] !== undefined && counter < max) { + stack.push(this.clusteredNodes[nodeId].node); + nodeId = this.clusteredNodes[nodeId].clusterId; + counter++; + } + stack.push(this.body.nodes[nodeId]); + return stack; + }, + writable: true, + configurable: true + }, + _getConnectedId: { + + + /** + * Get the Id the node is connected to + * @param edge + * @param nodeId + * @returns {*} + * @private + */ + value: function _getConnectedId(edge, nodeId) { + if (edge.toId != nodeId) { + return edge.toId; + } else if (edge.fromId != nodeId) { + return edge.fromId; + } else { + return edge.fromId; + } + }, + writable: true, + configurable: true + }, + _getHubSize: { + + /** + * We determine how many connections denote an important hub. + * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) + * + * @private + */ + value: function _getHubSize() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; + + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length > largestHub) { + largestHub = node.edges.length; + } + average += node.edges.length; + averageSquared += Math.pow(node.edges.length, 2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; + var variance = averageSquared - Math.pow(average, 2); + var standardDeviation = Math.sqrt(variance); + var hubThreshold = Math.floor(average + 2 * standardDeviation); - /** - * delete everything in the selection - * - * @private - */ - exports._deleteSelected = function() { - if (!this._selectionIsEmpty() && this.editMode == true) { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - if (this.triggerFunctions.del) { - var me = this; - var data = {nodes: selectedNodes, edges: selectedEdges}; - if (this.triggerFunctions.del.length == 2) { - this.triggerFunctions.del(data, function (finalizedData) { - me.edgesData.remove(finalizedData.edges); - me.nodesData.remove(finalizedData.nodes); - me._unselectAll(); - me.moving = true; - me.start(); - }); + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; } - else { - throw new Error('The function for delete does not support two arguments (data, callback)') - } - } - else { - this.edgesData.remove(selectedEdges); - this.nodesData.remove(selectedNodes); - this._unselectAll(); - this.moving = true; - this.start(); - } - } - else { - alert(this.constants.locales[this.constants.locale]["deleteClusterError"]); + + return hubThreshold; + }, + writable: true, + configurable: true } - } - }; + }); + + return ClusterEngine; + })(); + exports.ClusterEngine = ClusterEngine; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ }, -/* 68 */ +/* 74 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); - var Hammer = __webpack_require__(19); - - exports._cleanNavigation = function() { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].dispose(); - } - this.navigationHammers = []; - } + "use strict"; - this._navigationReleaseOverload = function () {}; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { - this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); - } - }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private + * Created by Alex on 26-Feb-15. */ - exports._loadNavigationElements = function() { - this._cleanNavigation(); - this.navigationDOM = {}; - var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; - var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent']; + if (typeof window !== "undefined") { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + } - this.navigationDOM['wrapper'] = document.createElement('div'); - this.frame.appendChild(this.navigationDOM['wrapper']); + var CanvasRenderer = (function () { + function CanvasRenderer(body) { + var _this = this; + _classCallCheck(this, CanvasRenderer); - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDOM[navigationDivs[i]] = document.createElement('div'); - this.navigationDOM[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i]; - this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]); + this.body = body; - var hammer = Hammer(this.navigationDOM[navigationDivs[i]], {prevent_default: true}); - hammer.on('touch', this[navigationDivActions[i]].bind(this)); - this.navigationHammers.push(hammer); + this.redrawRequested = false; + this.renderTimer = false; + this.requiresTimeout = true; + this.continueRendering = true; + this.renderRequests = 0; + + this.translation = { x: 0, y: 0 }; + this.scale = 1; + this.canvasTopLeft = { x: 0, y: 0 }; + this.canvasBottomRight = { x: 0, y: 0 }; + + this.body.emitter.on("_setScale", function (scale) { + return _this.scale = scale; + }); + this.body.emitter.on("_setTranslation", function (translation) { + _this.translation.x = translation.x;_this.translation.y = translation.y; + }); + this.body.emitter.on("_redraw", this._redraw.bind(this)); + this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true)); + this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); + this.body.emitter.on("_startRendering", function () { + _this.renderRequests += 1;_this.continueRendering = true;_this.startRendering(); + }); + this.body.emitter.on("_stopRendering", function () { + _this.renderRequests -= 1;_this.continueRendering = _this.renderRequests > 0; + }); + + this._determineBrowserMethod(); } - this._navigationReleaseOverload = this._stopMovement; + _prototypeProperties(CanvasRenderer, null, { + startRendering: { + value: function startRendering() { + if (this.continueRendering === true) { + if (!this.renderTimer) { + if (this.requiresTimeout == true) { + this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function + } else { + this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function + } + } + } else {} + }, + writable: true, + configurable: true + }, + renderStep: { + value: function renderStep() { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; - }; + if (this.requiresTimeout == true) { + // this schedules a new simulation step + this.startRendering(); + } + this._redraw(); - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._zoomExtent = function(event) { - this.zoomExtent({duration:700}); - event.stopPropagation(); - }; + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); + } + }, + writable: true, + configurable: true + }, + setCanvas: { + value: function setCanvas(canvas) { + this.canvas = canvas; + }, + writable: true, + configurable: true + }, + redraw: { + /** + * Redraw the network with the current data + * chart will be resized too. + */ + value: function redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); + }, + writable: true, + configurable: true + }, + _requestRedraw: { + + /** + * Redraw the network with the current data + * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. + * @private + */ + value: function _requestRedraw(hidden) { + if (this.redrawRequested !== true) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, hidden), 0); + } else { + window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); + } + } + }, + writable: true, + configurable: true + }, + _redraw: { + value: function _redraw() { + var hidden = arguments[0] === undefined ? false : arguments[0]; + this.body.emitter.emit("_beforeRender"); - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._stopMovement = function() { - this._xStopMoving(); - this._yStopMoving(); - this._stopZoom(); - }; + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext("2d"); + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. - * - * @private - */ - exports._moveUp = function(event) { - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + 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); - /** - * move the screen down - * @private - */ - exports._moveDown = function(event) { - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + this.canvasTopLeft = this.canvas.DOMtoCanvas({ x: 0, y: 0 }); + this.canvasBottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth, y: this.canvas.frame.canvas.clientHeight }); + if (hidden === false) { + // todo: solve this + //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { + this._drawEdges(ctx); + //} + } - /** - * move the screen left - * @private - */ - exports._moveLeft = function(event) { - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + // todo: solve this + //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { + this._drawNodes(ctx, this.body.nodes, hidden); + //} + if (hidden === false) { + if (this.controlNodesActive == true) { + this._drawControlNodes(ctx); + } + } - /** - * move the screen right - * @private - */ - exports._moveRight = function(event) { - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + //this._drawNodes(ctx,this.body.supportNodes,true); + // this.physics.nodesSolver._debug(ctx,"#F00F0F"); + // restore original scaling and translation + ctx.restore(); - /** - * Zoom in, using the same method as the movement. - * @private - */ - exports._zoomIn = function(event) { - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + if (hidden === true) { + ctx.clearRect(0, 0, w, h); + } + }, + writable: true, + configurable: true + }, + _drawNodes: { + + + /** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] + * @private + */ + value: function _drawNodes(ctx, nodes) { + var alwaysShow = arguments[2] === undefined ? false : arguments[2]; + // first draw the unselected nodes + var selected = []; + + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].setScaleAndPos(this.scale, this.canvasTopLeft, this.canvasBottomRight); + if (nodes[id].isSelected()) { + selected.push(id); + } else { + if (alwaysShow === true) { + nodes[id].draw(ctx); + } else if (nodes[id].inArea() === true) { + nodes[id].draw(ctx); + } + } + } + } + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + if (nodes[selected[s]].inArea() || alwaysShow) { + nodes[selected[s]].draw(ctx); + } + } + }, + writable: true, + configurable: true + }, + _drawEdges: { + + + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawEdges(ctx) { + var edges = this.body.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.setScale(this.scale); + if (edge.connected === true) { + edges[id].draw(ctx); + } + } + } + }, + writable: true, + configurable: true + }, + _drawControlNodes: { + + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawControlNodes(ctx) { + var edges = this.body.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + edges[id]._drawControlNodes(ctx); + } + } + }, + writable: true, + configurable: true + }, + _determineBrowserMethod: { + + /** + * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because + * some implementations (safari and IE9) did not support requestAnimationFrame + * @private + */ + value: function _determineBrowserMethod() { + if (typeof window !== "undefined") { + var browserType = navigator.userAgent.toLowerCase(); + this.requiresTimeout = false; + if (browserType.indexOf("msie 9.0") != -1) { + // IE 9 + this.requiresTimeout = true; + } else if (browserType.indexOf("safari") != -1) { + // safari + if (browserType.indexOf("chrome") <= -1) { + this.requiresTimeout = true; + } + } + } else { + this.requiresTimeout = true; + } + }, + writable: true, + configurable: true + } + }); - /** - * Zoom out - * @private - */ - exports._zoomOut = function(event) { - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + return CanvasRenderer; + })(); + exports.CanvasRenderer = CanvasRenderer; + Object.defineProperty(exports, "__esModule", { + value: true + }); - /** - * Stop zooming and unhighlight the zoom controls - * @private - */ - exports._stopZoom = function(event) { - this.zoomIncrement = 0; - event && event.preventDefault(); - }; +/***/ }, +/* 75 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - /** - * Stop moving in the Y direction and unHighlight the up and down - * @private - */ - exports._yStopMoving = function(event) { - this.yIncrement = 0; - event && event.preventDefault(); - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Stop moving in the X direction and unHighlight left and right. - * @private + * Created by Alex on 26-Feb-15. */ - exports._xStopMoving = function(event) { - this.xIncrement = 0; - event && event.preventDefault(); - }; -/***/ }, -/* 69 */ -/***/ function(module, exports, __webpack_require__) { + var Hammer = __webpack_require__(19); - exports._resetLevels = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.preassignedLevel == false) { - node.level = -1; - node.hierarchyEnumerated = false; - } + var Canvas = (function () { + /** + * Create the main frame for the Network. + * This function is executed once when a Network object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + * @private + */ + function Canvas(body, options) { + var _this = this; + _classCallCheck(this, Canvas); + + this.body = body; + this.setOptions(options); + + this.translation = { x: 0, y: 0 }; + this.scale = 1; + this.body.emitter.on("_setScale", function (scale) { + _this.scale = scale; + }); + this.body.emitter.on("_setTranslation", function (translation) { + _this.translation.x = translation.x;_this.translation.y = translation.y; + }); + this.body.emitter.once("resize", function (obj) { + _this.translation.x = obj.width * 0.5;_this.translation.y = obj.height * 0.5;_this.body.emitter.emit("_setTranslation", _this.translation); + }); + + this.pixelRatio = 1; + + // remove all elements from the container element. + while (this.body.container.hasChildNodes()) { + this.body.container.removeChild(this.body.container.firstChild); + } + + this.frame = document.createElement("div"); + this.frame.className = "vis network-frame"; + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; + this.frame.tabIndex = 900; + + ////////////////////////////////////////////////////////////////// + + this.frame.canvas = document.createElement("canvas"); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); + + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement("DIV"); + noCanvas.style.color = "red"; + noCanvas.style.fontWeight = "bold"; + noCanvas.style.padding = "10px"; + noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; + this.frame.canvas.appendChild(noCanvas); + } else { + var ctx = this.frame.canvas.getContext("2d"); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + + //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } + + // add the frame to the container element + this.body.container.appendChild(this.frame); + + this.body.emitter.emit("_setScale", 1);; + this.body.emitter.emit("_setTranslation", { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight });; + + this._bindHammer(); } - }; - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - exports._setupHierarchicalLayout = function() { - if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var hubsize = 0; - var node, nodeId; - var definedLevel = false; - var undefinedLevel = false; + _prototypeProperties(Canvas, null, { + _bindHammer: { + + + /** + * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * @private + */ + value: function _bindHammer() { + var me = this; + if (this.hammer !== undefined) { + this.hammer.dispose(); + } + this.drag = {}; + this.pinch = {}; + this.hammer = Hammer(this.frame.canvas, { + prevent_default: true + }); + this.hammer.on("tap", me.body.eventListeners.onTap); + this.hammer.on("doubletap", me.body.eventListeners.onDoubleTap); + this.hammer.on("hold", me.body.eventListeners.onHold); + this.hammer.on("touch", me.body.eventListeners.onTouch); + this.hammer.on("dragstart", me.body.eventListeners.onDragStart); + this.hammer.on("drag", me.body.eventListeners.onDrag); + this.hammer.on("dragend", me.body.eventListeners.onDragEnd); - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level != -1) { - definedLevel = true; + if (this.options.zoomable == true) { + this.hammer.on("mousewheel", me.body.eventListeners.onMouseWheel.bind(me)); + this.hammer.on("DOMMouseScroll", me.body.eventListeners.onMouseWheel.bind(me)); // for FF + this.hammer.on("pinch", me.body.eventListeners.onPinch.bind(me)); } - else { - undefinedLevel = true; + + this.hammer.on("mousemove", me.body.eventListeners.onMouseMove.bind(me)); + + this.hammerFrame = Hammer(this.frame, { + prevent_default: true + }); + this.hammerFrame.on("release", me.body.eventListeners.onRelease.bind(me)); + }, + writable: true, + configurable: true + }, + setOptions: { + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + this.options = options; + }, + writable: true, + configurable: true + }, + setSize: { + + /** + * Set a new size for the network + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') + */ + value: function setSize(width, height) { + var emitEvent = false; + var oldWidth = this.frame.canvas.width; + var oldHeight = this.frame.canvas.height; + if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; + + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + + this.options.width = width; + this.options.height = height; + + emitEvent = true; + } else { + // this would adapt the width of the canvas to the width from 100% if and only if + // there is a change. + + if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + emitEvent = true; + } + if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + emitEvent = true; + } } - if (hubsize < node.edges.length) { - hubsize = node.edges.length; + + if (emitEvent === true) { + this.body.emitter.emit("resize", { width: this.frame.canvas.width * this.pixelRatio, height: this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio }); } - } - } + }, + writable: true, + configurable: true + }, + _XconvertDOMtoCanvas: { + + + /** + * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} x + * @returns {number} + * @private + */ + value: function _XconvertDOMtoCanvas(x) { + return (x - this.translation.x) / this.scale; + }, + writable: true, + configurable: true + }, + _XconvertCanvasToDOM: { + + /** + * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the X coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} x + * @returns {number} + * @private + */ + value: function _XconvertCanvasToDOM(x) { + return x * this.scale + this.translation.x; + }, + writable: true, + configurable: true + }, + _YconvertDOMtoCanvas: { + + /** + * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} y + * @returns {number} + * @private + */ + value: function _YconvertDOMtoCanvas(y) { + return (y - this.translation.y) / this.scale; + }, + writable: true, + configurable: true + }, + _YconvertCanvasToDOM: { + + /** + * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} y + * @returns {number} + * @private + */ + value: function _YconvertCanvasToDOM(y) { + return y * this.scale + this.translation.y; + }, + writable: true, + configurable: true + }, + canvasToDOM: { - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - this.zoomExtent({duration:0},true,this.constants.clustering.enabled); - if (!this.constants.clustering.enabled) { - this.start(); - } + + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + value: function canvasToDOM(pos) { + return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; + }, + writable: true, + configurable: true + }, + DOMtoCanvas: { + + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + value: function DOMtoCanvas(pos) { + return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; + }, + writable: true, + configurable: true } - else { - // setup the system to use hierarchical method. - this._changeConstants(); + }); - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.constants.hierarchicalLayout.layout == "hubsize") { - this._determineLevels(hubsize); - } - else { - this._determineLevelsDirected(false); - } + return Canvas; + })(); - } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); + exports.Canvas = Canvas; + Object.defineProperty(exports, "__esModule", { + value: true + }); - // place the nodes on the canvas. This also stablilizes the system. - this._placeNodesByHierarchy(distribution); +/***/ }, +/* 76 */ +/***/ function(module, exports, __webpack_require__) { - // start the simulation. - this.start(); - } - } - }; + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() - * @private + * Created by Alex on 26-Feb-15. */ - exports._placeNodesByHierarchy = function(distribution) { - var nodeId, node; - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { + var util = __webpack_require__(1); - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { - node = distribution[level].nodes[nodeId]; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (node.xFixed) { - node.x = distribution[level].minPos; - node.xFixed = false; + var View = (function () { + function View(body, options) { + var _this = this; + _classCallCheck(this, View); - distribution[level].minPos += distribution[level].nodeSpacing; + this.body = body; + this.setOptions(options); + + this.animationSpeed = 1 / this.renderRefreshRate; + this.animationEasingFunction = "easeInOutQuint"; + this.easingTime = 0; + this.sourceScale = 0; + this.targetScale = 0; + this.sourceTranslation = 0; + this.targetTranslation = 0; + this.lockedOnNodeId = null; + this.lockedOnNodeOffset = null; + this.touchTime = 0; + + this.translation = { x: 0, y: 0 }; + this.scale = 1; + + this.viewFunction = undefined; + + this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); + this.body.emitter.on("_setScale", function (scale) { + return _this.scale = scale; + }); + this.body.emitter.on("_setTranslation", function (translation) { + _this.translation.x = translation.x;_this.translation.y = translation.y; + }); + this.body.emitter.on("animationFinished", function () { + _this.body.emitter.emit("_stopRendering"); + }); + this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); + } + + _prototypeProperties(View, null, { + setOptions: { + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + this.options = options; + }, + writable: true, + configurable: true + }, + setCanvas: { + value: function setCanvas(canvas) { + this.canvas = canvas; + }, + writable: true, + configurable: true + }, + _getRange: { + + // zoomExtent + /** + * Find the center position of the network + * @private + */ + value: function _getRange() { + var specificNodes = arguments[0] === undefined ? [] : arguments[0]; + var minY = 1000000000, + maxY = -1000000000, + minX = 1000000000, + maxX = -1000000000, + node; + if (specificNodes.length > 0) { + for (var i = 0; i < specificNodes.length; i++) { + node = this.body.nodes[specificNodes[i]]; + if (minX > node.boundingBox.left) { + minX = node.boundingBox.left; } + if (maxX < node.boundingBox.right) { + maxX = node.boundingBox.right; + } + if (minY > node.boundingBox.bottom) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.boundingBox.top) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive } - else { - if (node.yFixed) { - node.y = distribution[level].minPos; - node.yFixed = false; - - distribution[level].minPos += distribution[level].nodeSpacing; + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (minX > node.boundingBox.left) { + minX = node.boundingBox.left; + } + if (maxX < node.boundingBox.right) { + maxX = node.boundingBox.right; + } + if (minY > node.boundingBox.bottom) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.boundingBox.top) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive } } - this._placeBranchNodes(node.edges,node.id,distribution,node.level); } - } - } - } - // stabilize the system after positioning. This function calls zoomExtent. - this._stabilize(); - }; + if (minX == 1000000000 && maxX == -1000000000 && minY == 1000000000 && maxY == -1000000000) { + minY = 0, maxY = 0, minX = 0, maxX = 0; + } + return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; + }, + writable: true, + configurable: true + }, + _findCenter: { - /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} - * @private - */ - exports._getDistribution = function() { - var distribution = {}; - var nodeId, node, level; + /** + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private + */ + value: function _findCenter(range) { + return { x: 0.5 * (range.maxX + range.minX), + y: 0.5 * (range.maxY + range.minY) }; + }, + writable: true, + configurable: true + }, + zoomExtent: { + + + /** + * This function zooms out to fit all data on screen based on amount of nodes + * @param {Object} + * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; + * @param {Boolean} [disableStart] | If true, start is not called. + */ + value: function zoomExtent() { + var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; + var initialZoom = arguments[1] === undefined ? false : arguments[1]; + var range; + var zoomLevel; + + if (initialZoom == true) { + // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. + var positionDefined = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.predefinedPosition == true) { + positionDefined += 1; + } + } + } + if (positionDefined > 0.5 * this.body.nodeIndices.length) { + this.zoomExtent(options, false); + return; + } - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.xFixed = true; - node.yFixed = true; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; - } - else { - node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; - } - if (distribution[node.level] === undefined) { - distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; - } - distribution[node.level].amount += 1; - distribution[node.level].nodes[nodeId] = node; - } - } + range = this._getRange(options.nodes); - // determine the largest amount of nodes of all levels - var maxCount = 0; - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - if (maxCount < distribution[level].amount) { - maxCount = distribution[level].amount; - } - } - } + var numberOfNodes = this.body.nodeIndices.length; + if (this.options.smoothCurves == true) { + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } else { + zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } - // set the initial position and spacing of each nodes accordingly - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; - distribution[level].nodeSpacing /= (distribution[level].amount + 1); - distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); - } - } + // correct for larger canvasses. + var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); + zoomLevel *= factor; + } else { + this.body.emitter.emit("_redrawHidden"); + range = this._getRange(options.nodes); + var xDistance = Math.abs(range.maxX - range.minX) * 1.1; + var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - return distribution; - }; + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } + if (zoomLevel > 1) { + zoomLevel = 1; + } - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize - * @private - */ - exports._determineLevels = function(hubsize) { - var nodeId, node; - // determine hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.edges.length == hubsize) { - node.level = 0; - } - } - } + var center = this._findCenter(range); + var animationOptions = { position: center, scale: zoomLevel, animation: options }; + this.moveTo(animationOptions); + }, + writable: true, + configurable: true + }, + focusOnNode: { + + // animation + + /** + * Center a node in view. + * + * @param {Number} nodeId + * @param {Number} [options] + */ + value: function focusOnNode(nodeId) { + var options = arguments[1] === undefined ? {} : arguments[1]; + if (this.body.nodes[nodeId] !== undefined) { + var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; + options.position = nodePosition; + options.lockedOnNode = nodeId; + + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); + } + }, + writable: true, + configurable: true + }, + moveTo: { - // branch from hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level == 0) { - this._setLevel(1,node.edges,node.id); - } - } - } - }; + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.scale = Number // scale to move to + * | options.position = {x:Number, y:Number} // position to move to + * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to + */ + value: function moveTo(options) { + if (options === undefined) { + options = {}; + return; + } + if (options.offset === undefined) { + options.offset = { x: 0, y: 0 }; + } + if (options.offset.x === undefined) { + options.offset.x = 0; + } + if (options.offset.y === undefined) { + options.offset.y = 0; + } + if (options.scale === undefined) { + options.scale = this.scale; + } + if (options.position === undefined) { + options.position = this.translation; + } + if (options.animation === undefined) { + options.animation = { duration: 0 }; + } + if (options.animation === false) { + options.animation = { duration: 0 }; + } + if (options.animation === true) { + options.animation = {}; + } + if (options.animation.duration === undefined) { + options.animation.duration = 1000; + } // default duration + if (options.animation.easingFunction === undefined) { + options.animation.easingFunction = "easeInOutQuad"; + } // default easing function + this.animateView(options); + }, + writable: true, + configurable: true + }, + animateView: { + + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.time = Number // animation time in milliseconds + * | options.scale = Number // scale to animate to + * | options.position = {x:Number, y:Number} // position to animate to + * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, + * // easeInCubic, easeOutCubic, easeInOutCubic, + * // easeInQuart, easeOutQuart, easeInOutQuart, + * // easeInQuint, easeOutQuint, easeInOutQuint + */ + value: function animateView(options) { + if (options === undefined) { + return; + } + this.animationEasingFunction = options.animation.easingFunction; + // release if something focussed on the node + this.releaseNode(); + if (options.locked == true) { + this.lockedOnNodeId = options.lockedOnNode; + this.lockedOnNodeOffset = options.offset; + } + + // forcefully complete the old animation if it was still running + if (this.easingTime != 0) { + this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + } + + this.sourceScale = this.scale; + this.sourceTranslation = this.translation; + this.targetScale = options.scale; + + // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw + // but at least then we'll have the target transition + this.body.emitter.emit("_setScale", this.targetScale); + var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - options.position.x, + y: viewCenter.y - options.position.y + }; + this.targetTranslation = { + x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, + y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y + }; + // if the time is set to 0, don't do an animation + if (options.animation.duration == 0) { + if (this.lockedOnNodeId != null) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + } else { + this.body.emitter.emit("_setScale", this.targetScale);; + this.body.emitter.emit("_setTranslation", this.targetTranslation); + this.body.emitter.emit("_requestRedraw"); + } + } else { + this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's + this.animationEasingFunction = options.animation.easingFunction; - /** - * this function allocates nodes in levels based on the direction of the edges - * - * @param hubsize - * @private - */ - exports._determineLevelsDirected = function() { - var nodeId, node, firstNode; - var minLevel = 10000; - // set first node to source - firstNode = this.nodes[this.nodeIndices[0]]; - firstNode.level = minLevel; - this._setLevelDirected(minLevel,firstNode.edges,firstNode.id); + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + }, + writable: true, + configurable: true + }, + _lockedRedraw: { + + /** + * used to animate smoothly by hijacking the redraw function. + * @private + */ + value: function _lockedRedraw() { + var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; + var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - nodePosition.x, + y: viewCenter.y - nodePosition.y + }; + var sourceTranslation = this.translation; + var targetTranslation = { + x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, + y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y + }; - // get the minimum level - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - minLevel = node.level < minLevel ? node.level : minLevel; - } - } + this.body.emitter.emit("_setTranslation", targetTranslation); + }, + writable: true, + configurable: true + }, + releaseNode: { + value: function releaseNode() { + if (this.lockedOnNodeId !== undefined) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + } + }, + writable: true, + configurable: true + }, + _transitionRedraw: { + + /** + * + * @param easingTime + * @private + */ + value: function _transitionRedraw() { + var finished = arguments[0] === undefined ? false : arguments[0]; + this.easingTime += this.animationSpeed; + this.easingTime = finished === true ? 1 : this.easingTime; + + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + + this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress); + this.body.emitter.emit("_setTranslation", { + x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, + y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress + }); - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.level -= minLevel; + // cleanup + if (this.easingTime >= 1) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != null) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + } + this.body.emitter.emit("animationFinished"); + } + }, + writable: true, + configurable: true } - } - }; + }); + return View; + })(); - /** - * Since hierarchical layout does not support: - * - smooth curves (based on the physics), - * - clustering (based on dynamic node counts) - * - * We disable both features so there will be no problems. - * - * @private - */ - exports._changeConstants = function() { - this.constants.clustering.enabled = false; - this.constants.physics.barnesHut.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this._loadSelectedForceSolver(); - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.dynamic = false; - } - this._configureSmoothCurves(); + exports.View = View; + Object.defineProperty(exports, "__esModule", { + value: true + }); - var config = this.constants.hierarchicalLayout; - config.levelSeparation = Math.abs(config.levelSeparation); - if (config.direction == "RL" || config.direction == "DU") { - config.levelSeparation *= -1; - } +/***/ }, +/* 77 */ +/***/ function(module, exports, __webpack_require__) { - if (config.direction == "RL" || config.direction == "LR") { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "vertical"; - } - } - else { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "horizontal"; - } - } - }; + "use strict"; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. + * Created by Alex on 2/27/2015. * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel - * @private */ - exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - var nodeMoved = false; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (childNode.xFixed && childNode.level > parentLevel) { - childNode.xFixed = false; - childNode.x = distribution[childNode.level].minPos; - nodeMoved = true; - } - } - else { - if (childNode.yFixed && childNode.level > parentLevel) { - childNode.yFixed = false; - childNode.y = distribution[childNode.level].minPos; - nodeMoved = true; - } - } + var SelectionHandler = __webpack_require__(78).SelectionHandler; + var util = __webpack_require__(1); - if (nodeMoved == true) { - distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); - } - } + var TouchEventHandler = (function () { + function TouchEventHandler(body) { + var _this = this; + _classCallCheck(this, TouchEventHandler); + + this.body = body; + + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); + + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.pointerPosition = { x: 0, y: 0 }; + + this.scale = 1; + this.body.emitter.on("_setScale", function (scale) { + return _this.scale = scale; + }); + + this.selectionHandler = new SelectionHandler(body); } - }; + _prototypeProperties(TouchEventHandler, null, { + setCanvas: { + value: function setCanvas(canvas) { + this.canvas = canvas; + this.selectionHandler.setCanvas(canvas); + }, + writable: true, + configurable: true + }, + getPointer: { + + /** + * Get the pointer location from a touch location + * @param {{pageX: Number, pageY: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ + value: function getPointer(touch) { + return { + x: touch.pageX - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.pageY - util.getAbsoluteTop(this.canvas.frame.canvas) + }; + }, + writable: true, + configurable: true + }, + onTouch: { - /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. - * - * @param level - * @param edges - * @param parentId - * @private - */ - exports._setLevel = function(level, edges, parentId) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } - if (childNode.level == -1 || childNode.level > level) { - childNode.level = level; - if (childNode.edges.length > 1) { - this._setLevel(level+1, childNode.edges, childNode.id); - } - } - } - }; + /** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 100) { + this.drag.pointer = this.getPointer(event.gesture.center); + this.drag.pinched = false; + this.pinch.scale = this.scale; - /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction - * - * @param level - * @param edges - * @param parentId - * @private - */ - exports._setLevelDirected = function(level, edges, parentId) { - this.nodes[parentId].hierarchyEnumerated = true; - var childNode, direction; - for (var i = 0; i < edges.length; i++) { - direction = 1; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - direction = -1; - } - else { - childNode = edges[i].to; - } - if (childNode.level == -1) { - childNode.level = level + direction; - } - } + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); + } + }, + writable: true, + configurable: true + }, + onTap: { + + /** + * handle tap/click event: select/unselect a node + * @private + */ + value: function onTap(event) { + console.log("tap", event); + var pointer = this.getPointer(event.gesture.center); + this.pointerPosition = pointer; + this.selectionHandler.selectOnPoint(pointer); + }, + writable: true, + configurable: true + }, + onDragStart: { + + /** + * handle drag start event + * @private + */ + + /** + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ + value: function onDragStart(event) {}, + writable: true, + configurable: true + }, + onDrag: { - for (var i = 0; i < edges.length; i++) { - if (edges[i].toId == parentId) {childNode = edges[i].from;} - else {childNode = edges[i].to;} - if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { - this._setLevelDirected(childNode.level, childNode.edges, childNode.id); - } - } - }; + /** + * handle drag event + * @private + */ + value: function onDrag(event) {}, + writable: true, + configurable: true + }, + onDragEnd: { - /** - * Unfix nodes - * - * @private - */ - exports._restoreNodes = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.nodes[nodeId].xFixed = false; - this.nodes[nodeId].yFixed = false; - } - } - }; + /** + * handle drag start event + * @private + */ + value: function onDragEnd(event) {}, + writable: true, + configurable: true + }, + onDoubleTap: { + + /** + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) {}, + writable: true, + configurable: true + }, + onHold: { -/***/ }, -/* 70 */ -/***/ function(module, exports, __webpack_require__) { - // English - exports['en'] = { - edit: 'Edit', - del: 'Delete selected', - back: 'Back', - addNode: 'Add Node', - addEdge: 'Add Edge', - editNode: 'Edit Node', - editEdge: 'Edit Edge', - addDescription: 'Click in an empty space to place a new node.', - edgeDescription: 'Click on a node and drag the edge to another node to connect them.', - editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.', - createEdgeError: 'Cannot link edges to a cluster.', - deleteClusterError: 'Clusters cannot be deleted.' - }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) {}, + writable: true, + configurable: true + }, + onRelease: { - // Dutch - exports['nl'] = { - edit: 'Wijzigen', - del: 'Selectie verwijderen', - back: 'Terug', - addNode: 'Node toevoegen', - addEdge: 'Link toevoegen', - editNode: 'Node wijzigen', - editEdge: 'Link wijzigen', - addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.', - edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.', - editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.', - createEdgeError: 'Kan geen link maken naar een cluster.', - deleteClusterError: 'Clusters kunnen niet worden verwijderd.' - }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; + /** + * handle the release of the screen + * + * @private + */ + value: function onRelease(event) {}, + writable: true, + configurable: true + }, + onPinch: { + + + /** + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) {}, + writable: true, + configurable: true + }, + _zoom: { + + + /** + * Zoom the network in or out + * @param {Number} scale a number around 1, and between 0.01 and 10 + * @param {{x: Number, y: Number}} pointer Position on screen + * @return {Number} appliedScale scale is limited within the boundaries + * @private + */ + value: function _zoom(scale, pointer) {}, + writable: true, + configurable: true + }, + onMouseWheel: { + + + /** + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event + * @private + */ + value: function onMouseWheel(event) {}, + writable: true, + configurable: true + }, + onMouseMove: { + + + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) {}, + writable: true, + configurable: true + } + }); + + return TouchEventHandler; + })(); + + exports.TouchEventHandler = TouchEventHandler; + Object.defineProperty(exports, "__esModule", { + value: true + }); + + // in case the touch event was triggered on an external div, do the initial touch now. + //if (this.drag.pointer === undefined) { + // this.onTouch(event); + //} + // + //var node = this._getNodeAt(this.drag.pointer); + //// note: drag.pointer is set in onTouch to get the initial touch location + // + //this.drag.dragging = true; + //this.drag.selection = []; + //this.drag.translation = this._getTranslation(); + //this.drag.nodeId = null; + //this.draggingNodes = false; + // + //if (node != null && this.constants.dragNodes == true) { + // this.draggingNodes = true; + // this.drag.nodeId = node.id; + // // select the clicked node if not yet selected + // if (!node.isSelected()) { + // this._selectObject(node, false); + // } + // + // this.emit("dragStart", {nodeIds: this.getSelection().nodes}); + // + // // create an array with the selected nodes and their original location and status + // for (var objectId in this.selectionObj.nodes) { + // if (this.selectionObj.nodes.hasOwnProperty(objectId)) { + // var object = this.selectionObj.nodes[objectId]; + // var s = { + // id: object.id, + // node: object, + // + // // store original x, y, xFixed and yFixed, make the node temporarily Fixed + // x: object.x, + // y: object.y, + // xFixed: object.xFixed, + // yFixed: object.yFixed + // }; + // + // object.xFixed = true; + // object.yFixed = true; + // + // this.drag.selection.push(s); + // } + // } + //} + //if (this.drag.pinched) { + // return; + //} + // + //// remove the focus on node if it is focussed on by the focusOnNode + //this.releaseNode(); + // + //var pointer = this.getPointer(event.gesture.center); + //var me = this; + //var drag = this.drag; + //var selection = drag.selection; + //if (selection && selection.length && this.constants.dragNodes == true) { + // // calculate delta's and new location + // var deltaX = pointer.x - drag.pointer.x; + // var deltaY = pointer.y - drag.pointer.y; + // + // // update position of all selected nodes + // selection.forEach(function (s) { + // var node = s.node; + // + // if (!s.xFixed) { + // node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); + // } + // + // if (!s.yFixed) { + // node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); + // } + // }); + // + // + // // start _animationStep if not yet running + // if (!this.moving) { + // this.moving = true; + // this.start(); + // } + //} + //else { + // // move the network + // if (this.constants.dragNetwork == true) { + // // if the drag was not started properly because the click started outside the network div, start it now. + // if (this.drag.pointer === undefined) { + // this._handleDragStart(event); + // return; + // } + // var diffX = pointer.x - this.drag.pointer.x; + // var diffY = pointer.y - this.drag.pointer.y; + // + // this._setTranslation( + // this.drag.translation.x + diffX, + // this.drag.translation.y + diffY + // ); + // this._redraw(); + // } + //} + //this.drag.dragging = false; + //var selection = this.drag.selection; + //if (selection && selection.length) { + // selection.forEach(function (s) { + // // restore original xFixed and yFixed + // s.node.xFixed = s.xFixed; + // s.node.yFixed = s.yFixed; + // }); + // this.moving = true; + // this.start(); + //} + //else { + // this._redraw(); + //} + //if (this.draggingNodes == false) { + // this.emit("dragEnd", {nodeIds: []}); + //} + //else { + // this.emit("dragEnd", {nodeIds: this.getSelection().nodes}); + //} + //var pointer = this.getPointer(event.gesture.center); + //this._handleDoubleTap(pointer); + //var pointer = this.getPointer(event.gesture.center); + //this.pointerPosition = pointer; + //this._handleOnHold(pointer); + //var pointer = this.getPointer(event.gesture.center); + //this._handleOnRelease(pointer); + //var pointer = this.getPointer(event.gesture.center); + // + //this.drag.pinched = true; + //if (!('scale' in this.pinch)) { + // this.pinch.scale = 1; + //} + // + //// TODO: enabled moving while pinching? + //var scale = this.pinch.scale * event.gesture.scale; + //this._zoom(scale, pointer) + //if (this.constants.zoomable == true) { + // var scaleOld = this._getScale(); + // if (scale < 0.00001) { + // scale = 0.00001; + // } + // if (scale > 10) { + // scale = 10; + // } + // + // var preScaleDragPointer = null; + // if (this.drag !== undefined) { + // if (this.drag.dragging == true) { + // preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + // } + // } + // // + this.canvas.frame.canvas.clientHeight / 2 + // var translation = this._getTranslation(); + // + // var scaleFrac = scale / scaleOld; + // var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + // var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + // + // this._setScale(scale); + // this._setTranslation(tx, ty); + // + // if (preScaleDragPointer != null) { + // var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + // this.drag.pointer.x = postScaleDragPointer.x; + // this.drag.pointer.y = postScaleDragPointer.y; + // } + // + // this._redraw(); + // + // if (scaleOld < scale) { + // this.emit("zoom", {direction: "+"}); + // } + // else { + // this.emit("zoom", {direction: "-"}); + // } + // + // return scale; + //} + //// retrieve delta + //var delta = 0; + //if (event.wheelDelta) { /* IE/Opera. */ + // delta = event.wheelDelta / 120; + //} else if (event.detail) { /* Mozilla case. */ + // // In Mozilla, sign of delta is different than in IE. + // // Also, delta is multiple of 3. + // delta = -event.detail / 3; + //} + // + //// If delta is nonzero, handle it. + //// Basically, delta is now positive if wheel was scrolled up, + //// and negative, if wheel was scrolled down. + //if (delta) { + // + // // calculate the new scale + // var scale = this._getScale(); + // var zoom = delta / 10; + // if (delta < 0) { + // zoom = zoom / (1 - zoom); + // } + // scale *= (1 + zoom); + // + // // calculate the pointer location + // var gesture = hammerUtil.fakeGesture(this, event); + // var pointer = this.getPointer(gesture.center); + // + // // apply the new scale + // this._zoom(scale, pointer); + //} + // + //// Prevent default actions caused by mouse wheel. + //event.preventDefault(); + //var gesture = hammerUtil.fakeGesture(this, event); + //var pointer = this.getPointer(gesture.center); + //var popupVisible = false; + // + //// check if the previously selected node is still selected + //if (this.popup !== undefined) { + // if (this.popup.hidden === false) { + // this._checkHidePopup(pointer); + // } + // + // // if the popup was not hidden above + // if (this.popup.hidden === false) { + // popupVisible = true; + // this.popup.setPosition(pointer.x + 3, pointer.y - 5) + // this.popup.show(); + // } + //} + // + //// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over + //if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { + // this.canvas.frame.focus(); + //} + // + //// start a timeout that will check if the mouse is positioned above an element + //if (popupVisible === false) { + // var me = this; + // var checkShow = function() { + // me._checkShowPopup(pointer); + // }; + // + // if (this.popupTimer) { + // clearInterval(this.popupTimer); // stop any running calculationTimer + // } + // if (!this.drag.dragging) { + // this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); + // } + //} + // + ///** + // * Adding hover highlights + // */ + //if (this.constants.hover == true) { + // // removing all hover highlights + // for (var edgeId in this.hoverObj.edges) { + // if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + // this.hoverObj.edges[edgeId].hover = false; + // delete this.hoverObj.edges[edgeId]; + // } + // } + // + // // adding hover highlights + // var obj = this._getNodeAt(pointer); + // if (obj == null) { + // obj = this._getEdgeAt(pointer); + // } + // if (obj != null) { + // this._hoverObject(obj); + // } + // + // // removing all node hover highlights except for the selected one. + // for (var nodeId in this.hoverObj.nodes) { + // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { + // this._blurObject(this.hoverObj.nodes[nodeId]); + // delete this.hoverObj.nodes[nodeId]; + // } + // } + // } + // this.redraw(); + //} /***/ }, -/* 71 */ +/* 78 */ /***/ function(module, exports, __webpack_require__) { + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** - * Canvas shapes used by Network + * Created by Alex on 2/27/2015. */ - if (typeof CanvasRenderingContext2D !== 'undefined') { - /** - * Draw a circle shape - */ - CanvasRenderingContext2D.prototype.circle = function(x, y, r) { - this.beginPath(); - this.arc(x, y, r, 0, 2*Math.PI, false); - }; + var Node = __webpack_require__(56); - /** - * Draw a square shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r size, width and height of the square - */ - CanvasRenderingContext2D.prototype.square = function(x, y, r) { - this.beginPath(); - this.rect(x - r, y - r, r * 2, r * 2); - }; + var SelectionHandler = (function () { + function SelectionHandler(body) { + _classCallCheck(this, SelectionHandler); - /** - * Draw a triangle shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); + this.body = body; + this.selectionObj = { nodes: [], edges: [] }; - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height + this.options = { + select: true, + selectConnectedEdges: true + }; + } + + _prototypeProperties(SelectionHandler, null, { + setCanvas: { + value: function setCanvas(canvas) { + this.canvas = canvas; + }, + writable: true, + configurable: true + }, + selectOnPoint: { + + + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + if (this.options.select === true) { + if (this._getSelectedObjectCount() > 0) { + this._unselectAll(); + } - this.moveTo(x, y - (h - ir)); - this.lineTo(x + s2, y + ir); - this.lineTo(x - s2, y + ir); - this.lineTo(x, y - (h - ir)); - this.closePath(); - }; + this.selectObject(pointer); + this._generateClickEvent(pointer); - /** - * Draw a triangle shape in downward orientation - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius - */ - CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); + this.body.emitter.emit("_requestRedraw"); + } + }, + writable: true, + configurable: true + }, + _generateClickEvent: { + value: function _generateClickEvent(pointer) { + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: this.canvas.DOMtoCanvas(pointer) + }; + this.body.emitter.emit("click", properties); + }, + writable: true, + configurable: true + }, + selectObject: { + value: function selectObject(pointer) { + var obj = this._getNodeAt(pointer); + if (obj != null) { + if (this.options.selectConnectedEdges === true) { + this._selectConnectedEdges(obj); + } + } else { + obj = this._getEdgeAt(pointer); + } - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height + if (obj !== null) { + obj.select(); + this._addToSelection(obj); + this.body.emitter.emit("selected", this.getSelection()); + } + return obj; + }, + writable: true, + configurable: true + }, + _getNodesOverlappingWith: { + + + /** + * + * @param object + * @param overlappingNodes + * @private + */ + value: function _getNodesOverlappingWith(object, overlappingNodes) { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } + }, + writable: true, + configurable: true + }, + _getAllNodesOverlappingWith: { + + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + this._getNodesOverlappingWith(object, overlappingNodes); + return overlappingNodes; + }, + writable: true, + configurable: true + }, + _pointerToPositionObject: { + + + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x, + top: canvasPos.y, + right: canvasPos.x, + bottom: canvasPos.y + }; + }, + writable: true, + configurable: true + }, + _getNodeAt: { + + + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + value: function _getNodeAt(pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } else { + return null; + } + }, + writable: true, + configurable: true + }, + _getEdgesOverlappingWith: { + + + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } + }, + writable: true, + configurable: true + }, + _getAllEdgesOverlappingWith: { + + + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; + }, + writable: true, + configurable: true + }, + _getEdgeAt: { + + + /** + * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call + * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {null} + * @private + */ + value: function _getEdgeAt(pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return null; + } + }, + writable: true, + configurable: true + }, + _addToSelection: { + + + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; + } + }, + writable: true, + configurable: true + }, + _addToHover: { + + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; + } + }, + writable: true, + configurable: true + }, + _removeFromSelection: { + + + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + value: function _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } else { + delete this.selectionObj.edges[obj.id]; + } + }, + writable: true, + configurable: true + }, + _unselectAll: { + + /** + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + value: function _unselectAll() { + var doNotTrigger = arguments[0] === undefined ? false : arguments[0]; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; + this.selectionObj = { nodes: {}, edges: {} }; - /** - * Draw a star shape, a star with 5 points - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.star = function(x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); + if (doNotTrigger == false) { + this.body.emitter.emit("select", this.getSelection()); + } + }, + writable: true, + configurable: true + }, + _getSelectedNodeCount: { + + + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + value: function _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; + }, + writable: true, + configurable: true + }, + _getSelectedNode: { + + /** + * return the selected node + * + * @returns {number} + * @private + */ + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return null; + }, + writable: true, + configurable: true + }, + _getSelectedEdge: { + + /** + * return the selected edge + * + * @returns {number} + * @private + */ + value: function _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return null; + }, + writable: true, + configurable: true + }, + _getSelectedEdgeCount: { + + + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + value: function _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + }, + writable: true, + configurable: true + }, + _getSelectedObjectCount: { + + + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + }, + writable: true, + configurable: true + }, + _selectionIsEmpty: { + + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; + }, + writable: true, + configurable: true + }, + _clusterInSelection: { + + + /** + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + value: function _clusterInSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } + } + } + return false; + }, + writable: true, + configurable: true + }, + _selectConnectedEdges: { + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); + } + }, + writable: true, + configurable: true + }, + _hoverConnectedEdges: { + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } + }, + writable: true, + configurable: true + }, + _unselectConnectedEdges: { - for (var n = 0; n < 10; n++) { - var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; - this.lineTo( - x + radius * Math.sin(n * 2 * Math.PI / 10), - y - radius * Math.cos(n * 2 * Math.PI / 10) - ); - } - this.closePath(); - }; + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } + }, + writable: true, + configurable: true + }, + _blurObject: { - /** - * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - */ - CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { - var r2d = Math.PI/180; - if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x - if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y - this.beginPath(); - this.moveTo(x+r,y); - this.lineTo(x+w-r,y); - this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); - this.lineTo(x+w,y+h-r); - this.arc(x+w-r,y+h-r,r,0,r2d*90,false); - this.lineTo(x+r,y+h); - this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); - this.lineTo(x,y+r); - this.arc(x+r,y+r,r,r2d*180,r2d*270,false); - }; - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { - var kappa = .5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - this.beginPath(); - this.moveTo(x, ym); - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - }; - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { - var f = 1/3; - var wEllipse = w; - var hEllipse = h * f; + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function _blurObject(object) { + if (object.hover == true) { + object.hover = false; + this.body.emitter.emit("blurNode", { node: object.id }); + } + }, + writable: true, + configurable: true + }, + _hoverObject: { + + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function _hoverObject(object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.body.emitter.emit("hoverNode", { node: object.id }); + } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); + } + }, + writable: true, + configurable: true + }, + _handleDoubleTap: { - var kappa = .5522848, - ox = (wEllipse / 2) * kappa, // control point offset horizontal - oy = (hEllipse / 2) * kappa, // control point offset vertical - xe = x + wEllipse, // x-end - ye = y + hEllipse, // y-end - xm = x + wEllipse / 2, // x-middle - ym = y + hEllipse / 2, // y-middle - ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse - yeb = y + h; // y-end, bottom ellipse - this.beginPath(); - this.moveTo(xe, ym); - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + /** + * handles the selection part of the double tap and opens a cluster if needed + * + * @param {Object} pointer + * @private + */ + value: function _handleDoubleTap(pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = { x: this._XconvertDOMtoCanvas(pointer.x), + y: this._YconvertDOMtoCanvas(pointer.y) }; + this.openCluster(node); + } + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } + }; + this.body.emitter.emit("doubleClick", properties); + }, + writable: true, + configurable: true + }, + _handleOnHold: { + + + /** + * Handle the onHold selection part + * + * @param pointer + * @private + */ + value: function _handleOnHold(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node, true); + } else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge, true); + } + } + this._requestRedraw(); + }, + writable: true, + configurable: true + }, + getSelection: { + + + /** + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection + */ + value: function getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; + }, + writable: true, + configurable: true + }, + getSelectedNodes: { + + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedNodes() { + var idArray = []; + if (this.options.select == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + } + return idArray; + }, + writable: true, + configurable: true + }, + getSelectedEdges: { + + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedEdges() { + var idArray = []; + if (this.options.select == true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } + } + } + return idArray; + }, + writable: true, + configurable: true + }, + selectNodes: { - this.lineTo(xe, ymb); - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + /** + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] + */ + value: function selectNodes(selection, highlightEdges) { + var i, iMax, id; - this.lineTo(x, ym); - }; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + // first unselect any selected node + this._unselectAll(true); - /** - * Draw an arrow point (no line) - */ - CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { - // tail - var xt = x - length * Math.cos(angle); - var yt = y - length * Math.sin(angle); + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - // inner tail - // TODO: allow to customize different shapes - var xi = x - length * 0.9 * Math.cos(angle); - var yi = y - length * 0.9 * Math.sin(angle); + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); + } + this._selectObject(node, true, true, highlightEdges, true); + } + this.redraw(); + }, + writable: true, + configurable: true + }, + selectEdges: { - // left - var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); - var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); - // right - var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); - var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + value: function selectEdges(selection) { + var i, iMax, id; - this.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - /** - * Sets up the dashedLine functionality for drawing - * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas - * @author David Jordan - * @date 2012-08-08 - */ - CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ - if (!dashArray) dashArray=[10,5]; - if (dashLength==0) dashLength = 0.001; // Hack for Safari - var dashCount = dashArray.length; - this.moveTo(x, y); - var dx = (x2-x), dy = (y2-y); - var slope = dy/dx; - var distRemaining = Math.sqrt( dx*dx + dy*dy ); - var dashIndex=0, draw=true; - while (distRemaining>=0.1){ - var dashLength = dashArray[dashIndex++%dashCount]; - if (dashLength > distRemaining) dashLength = distRemaining; - var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); - if (dx<0) xStep = -xStep; - x += xStep; - y += slope*xStep; - this[draw ? 'lineTo' : 'moveTo'](x,y); - distRemaining -= dashLength; - draw = !draw; + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); + } + this._selectObject(edge, true, true, false, true); + } + this.redraw(); + }, + writable: true, + configurable: true + }, + _updateSelection: { + + /** + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + value: function _updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } + } + }, + writable: true, + configurable: true } - }; + }); - // TODO: add diamond shape - } + return SelectionHandler; + })(); + exports.SelectionHandler = SelectionHandler; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ } /******/ ]) diff --git a/dist/vis.min.css b/dist/vis.min.css index ce5ab00d..f624ec20 100644 --- a/dist/vis.min.css +++ b/dist/vis.min.css @@ -1 +1 @@ -.vis .overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-active{box-shadow:0 0 10px #86d5f8}.vis [class*=span]{min-height:0;width:auto}.vis.timeline.root{position:relative;border:1px solid #bfbfbf;overflow:hidden;padding:0;margin:0;box-sizing:border-box}.vis.timeline .vispanel{position:absolute;padding:0;margin:0;box-sizing:border-box}.vis.timeline .vispanel.bottom,.vis.timeline .vispanel.center,.vis.timeline .vispanel.left,.vis.timeline .vispanel.right,.vis.timeline .vispanel.top{border:1px #bfbfbf}.vis.timeline .vispanel.center,.vis.timeline .vispanel.left,.vis.timeline .vispanel.right{border-top-style:solid;border-bottom-style:solid;overflow:hidden}.vis.timeline .vispanel.bottom,.vis.timeline .vispanel.center,.vis.timeline .vispanel.top{border-left-style:solid;border-right-style:solid}.vis.timeline .background{overflow:hidden}.vis.timeline .vispanel>.content{position:relative}.vis.timeline .vispanel .shadow{position:absolute;width:100%;height:1px;box-shadow:0 0 10px rgba(0,0,0,.8)}.vis.timeline .vispanel .shadow.top{top:-1px;left:0}.vis.timeline .vispanel .shadow.bottom{bottom:-1px;left:0}.vis.timeline .labelset{position:relative;overflow:hidden;box-sizing:border-box}.vis.timeline .labelset .vlabel{position:relative;left:0;top:0;width:100%;color:#4d4d4d;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis.timeline .labelset .vlabel:last-child{border-bottom:none}.vis.timeline .labelset .vlabel .inner{display:inline-block;padding:5px}.vis.timeline .labelset .vlabel .inner.hidden{padding:0}.vis.timeline .itemset{position:relative;padding:0;margin:0;box-sizing:border-box}.vis.timeline .itemset .background,.vis.timeline .itemset .foreground{position:absolute;width:100%;height:100%;overflow:visible}.vis.timeline .axis{position:absolute;width:100%;height:0;left:0;z-index:1}.vis.timeline .foreground .group{position:relative;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis.timeline .foreground .group:last-child{border-bottom:none}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;border-width:1px;background-color:#D5DDF6;display:inline-block;padding:5px}.vis.timeline .item.selected{border-color:#FFC200;background-color:#FFF785;z-index:2}.vis.timeline .editable .item.selected{cursor:move}.vis.timeline .item.point.selected{background-color:#FFF785}.vis.timeline .item.box{text-align:center;border-style:solid;border-radius:2px}.vis.timeline .item.point{background:0 0}.vis.timeline .item.dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis.timeline .item.range{border-style:solid;border-radius:2px;box-sizing:border-box}.vis.timeline .item.background{overflow:hidden;border:none;background-color:rgba(213,221,246,.4);box-sizing:border-box;padding:0;margin:0}.vis.timeline .item.range .content{position:relative;display:inline-block;max-width:100%;overflow:hidden}.vis.timeline .item.background .content{position:absolute;display:inline-block;overflow:hidden;max-width:100%;margin:5px}.vis.timeline .item.line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis.timeline .item .content{white-space:nowrap;overflow:hidden}.vis.timeline .item .delete{background:url(img/timeline/delete.png) top center no-repeat;position:absolute;width:24px;height:24px;top:0;right:-24px;cursor:pointer}.vis.timeline .item.range .drag-left{position:absolute;width:24px;height:100%;top:0;left:-4px;cursor:w-resize}.vis.timeline .item.range .drag-right{position:absolute;width:24px;height:100%;top:0;right:-4px;cursor:e-resize}.vis.timeline .timeaxis{position:relative;overflow:hidden}.vis.timeline .timeaxis.foreground{top:0;left:0;width:100%}.vis.timeline .timeaxis.background{position:absolute;top:0;left:0;width:100%;height:100%}.vis.timeline .timeaxis .text{position:absolute;color:#4d4d4d;padding:3px;white-space:nowrap}.vis.timeline .timeaxis .text.measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis.timeline .timeaxis .grid.vertical{position:absolute;border-left:1px solid}.vis.timeline .timeaxis .grid.minor{border-color:#e5e5e5}.vis.timeline .timeaxis .grid.major{border-color:#bfbfbf}.vis.timeline .currenttime{background-color:#FF7F6E;width:2px;z-index:1}.vis.timeline .customtime{background-color:#6E94FF;width:2px;cursor:move;z-index:1}.vis.timeline .vispanel.background.horizontal .grid.horizontal{position:absolute;width:100%;height:0;border-bottom:1px solid}.vis.timeline .vispanel.background.horizontal .grid.minor{border-color:#e5e5e5}.vis.timeline .vispanel.background.horizontal .grid.major{border-color:#bfbfbf}.vis.timeline .dataaxis .yAxis.major{width:100%;position:absolute;color:#4d4d4d;white-space:nowrap}.vis.timeline .dataaxis .yAxis.major.measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.minor{position:absolute;width:100%;color:#bebebe;white-space:nowrap}.vis.timeline .dataaxis .yAxis.minor.measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.title{position:absolute;color:#4d4d4d;white-space:nowrap;bottom:20px;text-align:center}.vis.timeline .dataaxis .yAxis.title.measure{padding:0;margin:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.title.left{bottom:0;-webkit-transform-origin:left top;-moz-transform-origin:left top;-ms-transform-origin:left top;-o-transform-origin:left top;transform-origin:left bottom;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.vis.timeline .dataaxis .yAxis.title.right{bottom:0;-webkit-transform-origin:right bottom;-moz-transform-origin:right bottom;-ms-transform-origin:right bottom;-o-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.vis.timeline .legend{background-color:rgba(247,252,255,.65);padding:5px;border-color:#b3b3b3;border-style:solid;border-width:1px;box-shadow:2px 2px 10px rgba(154,154,154,.55)}.vis.timeline .legendText{white-space:nowrap;display:inline-block}.vis.timeline .graphGroup0{fill:#4f81bd;fill-opacity:0;stroke-width:2px;stroke:#4f81bd}.vis.timeline .graphGroup1{fill:#f79646;fill-opacity:0;stroke-width:2px;stroke:#f79646}.vis.timeline .graphGroup2{fill:#8c51cf;fill-opacity:0;stroke-width:2px;stroke:#8c51cf}.vis.timeline .graphGroup3{fill:#75c841;fill-opacity:0;stroke-width:2px;stroke:#75c841}.vis.timeline .graphGroup4{fill:#ff0100;fill-opacity:0;stroke-width:2px;stroke:#ff0100}.vis.timeline .graphGroup5{fill:#37d8e6;fill-opacity:0;stroke-width:2px;stroke:#37d8e6}.vis.timeline .graphGroup6{fill:#042662;fill-opacity:0;stroke-width:2px;stroke:#042662}.vis.timeline .graphGroup7{fill:#00ff26;fill-opacity:0;stroke-width:2px;stroke:#00ff26}.vis.timeline .graphGroup8{fill:#f0f;fill-opacity:0;stroke-width:2px;stroke:#f0f}.vis.timeline .graphGroup9{fill:#8f3938;fill-opacity:0;stroke-width:2px;stroke:#8f3938}.vis.timeline .fill{fill-opacity:.1;stroke:none}.vis.timeline .bar{fill-opacity:.5;stroke-width:1px}.vis.timeline .point{stroke-width:2px;fill-opacity:1}.vis.timeline .legendBackground{stroke-width:1px;fill-opacity:.9;fill:#fff;stroke:#c2c2c2}.vis.timeline .outline{stroke-width:1px;fill-opacity:1;fill:#fff;stroke:#e5e5e5}.vis.timeline .iconFill{fill-opacity:.3;stroke:none}div.network-manipulationDiv{border-width:0;border-bottom:1px;border-style:solid;border-color:#d6d9d8;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(to bottom,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#fcfcfc', GradientType=0);position:absolute;left:0;top:0;width:100%;height:30px}div.network-manipulation-editMode{position:absolute;left:0;top:15px;height:30px}div.network-manipulation-closeDiv{position:absolute;right:0;top:0;width:30px;height:30px;background-position:20px 3px;background-repeat:no-repeat;background-image:url(img/network/cross.png);cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.network-manipulation-closeDiv:hover{opacity:.6}div.network-manipulationUI{position:relative;top:-7px;font-family:verdana;font-size:12px;-moz-border-radius:15px;border-radius:15px;display:inline-block;background-position:0 0;background-repeat:no-repeat;height:24px;margin:0 0 0 10px;vertical-align:middle;cursor:pointer;padding:0 8px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.network-manipulationUI:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}div.network-manipulationUI:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}div.network-manipulationUI.back{background-image:url(img/network/backIcon.png)}div.network-manipulationUI.none:hover{box-shadow:1px 1px 8px transparent;cursor:default}div.network-manipulationUI.none:active{box-shadow:1px 1px 8px transparent}div.network-manipulationUI.none{padding:0}div.network-manipulationUI.notification{margin:2px;font-weight:700}div.network-manipulationUI.add{background-image:url(img/network/addNodeIcon.png)}div.network-manipulationUI.edit{background-image:url(img/network/editIcon.png)}div.network-manipulationUI.edit.editmode{background-color:#fcfcfc;border-style:solid;border-width:1px;border-color:#ccc}div.network-manipulationUI.connect{background-image:url(img/network/connectIcon.png)}div.network-manipulationUI.delete{background-image:url(img/network/deleteIcon.png)}div.network-manipulationLabel{margin:0 0 0 23px;line-height:25px}div.network-seperatorLine{display:inline-block;width:1px;height:20px;background-color:#bdbdbd;margin:5px 7px 0 15px}div.network-navigation_wrapper{position:absolute;left:0;top:0;width:100%;height:100%}div.network-navigation{width:34px;height:34px;-moz-border-radius:17px;border-radius:17px;position:absolute;display:inline-block;background-position:2px 2px;background-repeat:no-repeat;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.network-navigation:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.network-navigation:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.network-navigation.up{background-image:url(img/network/upArrow.png);bottom:50px;left:55px}div.network-navigation.down{background-image:url(img/network/downArrow.png);bottom:10px;left:55px}div.network-navigation.left{background-image:url(img/network/leftArrow.png);bottom:10px;left:15px}div.network-navigation.right{background-image:url(img/network/rightArrow.png);bottom:10px;left:95px}div.network-navigation.zoomIn{background-image:url(img/network/plus.png);bottom:10px;right:15px}div.network-navigation.zoomOut{background-image:url(img/network/minus.png);bottom:10px;right:55px}div.network-navigation.zoomExtends{background-image:url(img/network/zoomExtends.png);bottom:50px;right:15px}div.network-tooltip{position:absolute;visibility:hidden;padding:5px;white-space:nowrap;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border:1px solid;box-shadow:3px 3px 10px rgba(128,128,128,.5)} \ No newline at end of file +.vis .overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-active{box-shadow:0 0 10px #86d5f8}.vis [class*=span]{min-height:0;width:auto}.vis.timeline.root{position:relative;border:1px solid #bfbfbf;overflow:hidden;padding:0;margin:0;box-sizing:border-box}.vis.timeline .vispanel{position:absolute;padding:0;margin:0;box-sizing:border-box}.vis.timeline .vispanel.bottom,.vis.timeline .vispanel.center,.vis.timeline .vispanel.left,.vis.timeline .vispanel.right,.vis.timeline .vispanel.top{border:1px #bfbfbf}.vis.timeline .vispanel.center,.vis.timeline .vispanel.left,.vis.timeline .vispanel.right{border-top-style:solid;border-bottom-style:solid;overflow:hidden}.vis.timeline .vispanel.bottom,.vis.timeline .vispanel.center,.vis.timeline .vispanel.top{border-left-style:solid;border-right-style:solid}.vis.timeline .background{overflow:hidden}.vis.timeline .vispanel>.content{position:relative}.vis.timeline .vispanel .shadow{position:absolute;width:100%;height:1px;box-shadow:0 0 10px rgba(0,0,0,.8)}.vis.timeline .vispanel .shadow.top{top:-1px;left:0}.vis.timeline .vispanel .shadow.bottom{bottom:-1px;left:0}.vis.timeline .labelset{position:relative;overflow:hidden;box-sizing:border-box}.vis.timeline .labelset .vlabel{position:relative;left:0;top:0;width:100%;color:#4d4d4d;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis.timeline .labelset .vlabel:last-child{border-bottom:none}.vis.timeline .labelset .vlabel .inner{display:inline-block;padding:5px}.vis.timeline .labelset .vlabel .inner.hidden{padding:0}.vis.timeline .itemset{position:relative;padding:0;margin:0;box-sizing:border-box}.vis.timeline .itemset .background,.vis.timeline .itemset .foreground{position:absolute;width:100%;height:100%;overflow:visible}.vis.timeline .axis{position:absolute;width:100%;height:0;left:0;z-index:1}.vis.timeline .foreground .group{position:relative;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis.timeline .foreground .group:last-child{border-bottom:none}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;border-width:1px;background-color:#D5DDF6;display:inline-block;overflow:hidden}.vis.timeline .item.selected{border-color:#FFC200;background-color:#FFF785;z-index:2}.vis.timeline .editable .item.selected{cursor:move}.vis.timeline .item.point.selected{background-color:#FFF785}.vis.timeline .item.box{text-align:center;border-style:solid;border-radius:2px}.vis.timeline .item.point{background:0 0}.vis.timeline .item.dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis.timeline .item.range{border-style:solid;border-radius:2px;box-sizing:border-box}.vis.timeline .item.background{border:none;background-color:rgba(213,221,246,.4);box-sizing:border-box;padding:0;margin:0}.vis.timeline .item.range .content{position:relative;display:inline-block;max-width:100%}.vis.timeline .item.background .content{position:absolute;display:inline-block;max-width:100%;margin:5px}.vis.timeline .item.line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis.timeline .item .content{white-space:nowrap;box-sizing:border-box;padding:5px}.vis.timeline .item .delete{background:url(img/timeline/delete.png) top center no-repeat;position:absolute;width:24px;height:24px;top:0;right:-24px;cursor:pointer}.vis.timeline .item.range .drag-left{position:absolute;width:24px;height:100%;top:0;left:-4px;cursor:w-resize}.vis.timeline .item.range .drag-right{position:absolute;width:24px;height:100%;top:0;right:-4px;cursor:e-resize}.vis.timeline .timeaxis{position:relative;overflow:hidden}.vis.timeline .timeaxis.foreground{top:0;left:0;width:100%}.vis.timeline .timeaxis.background{position:absolute;top:0;left:0;width:100%;height:100%}.vis.timeline .timeaxis .text{position:absolute;color:#4d4d4d;padding:3px;white-space:nowrap}.vis.timeline .timeaxis .text.measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis.timeline .timeaxis .grid.vertical{position:absolute;border-left:1px solid}.vis.timeline .timeaxis .grid.minor{border-color:#e5e5e5}.vis.timeline .timeaxis .grid.major{border-color:#bfbfbf}.vis.timeline .currenttime{background-color:#FF7F6E;width:2px;z-index:1}.vis.timeline .customtime{background-color:#6E94FF;width:2px;cursor:move;z-index:1}.vis.timeline .vispanel.background.horizontal .grid.horizontal{position:absolute;width:100%;height:0;border-bottom:1px solid}.vis.timeline .vispanel.background.horizontal .grid.minor{border-color:#e5e5e5}.vis.timeline .vispanel.background.horizontal .grid.major{border-color:#bfbfbf}.vis.timeline .dataaxis .yAxis.major{width:100%;position:absolute;color:#4d4d4d;white-space:nowrap}.vis.timeline .dataaxis .yAxis.major.measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.minor{position:absolute;width:100%;color:#bebebe;white-space:nowrap}.vis.timeline .dataaxis .yAxis.minor.measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.title{position:absolute;color:#4d4d4d;white-space:nowrap;bottom:20px;text-align:center}.vis.timeline .dataaxis .yAxis.title.measure{padding:0;margin:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.title.left{bottom:0;-webkit-transform-origin:left top;-moz-transform-origin:left top;-ms-transform-origin:left top;-o-transform-origin:left top;transform-origin:left bottom;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.vis.timeline .dataaxis .yAxis.title.right{bottom:0;-webkit-transform-origin:right bottom;-moz-transform-origin:right bottom;-ms-transform-origin:right bottom;-o-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.vis.timeline .legend{background-color:rgba(247,252,255,.65);padding:5px;border-color:#b3b3b3;border-style:solid;border-width:1px;box-shadow:2px 2px 10px rgba(154,154,154,.55)}.vis.timeline .legendText{white-space:nowrap;display:inline-block}.vis.timeline .graphGroup0{fill:#4f81bd;fill-opacity:0;stroke-width:2px;stroke:#4f81bd}.vis.timeline .graphGroup1{fill:#f79646;fill-opacity:0;stroke-width:2px;stroke:#f79646}.vis.timeline .graphGroup2{fill:#8c51cf;fill-opacity:0;stroke-width:2px;stroke:#8c51cf}.vis.timeline .graphGroup3{fill:#75c841;fill-opacity:0;stroke-width:2px;stroke:#75c841}.vis.timeline .graphGroup4{fill:#ff0100;fill-opacity:0;stroke-width:2px;stroke:#ff0100}.vis.timeline .graphGroup5{fill:#37d8e6;fill-opacity:0;stroke-width:2px;stroke:#37d8e6}.vis.timeline .graphGroup6{fill:#042662;fill-opacity:0;stroke-width:2px;stroke:#042662}.vis.timeline .graphGroup7{fill:#00ff26;fill-opacity:0;stroke-width:2px;stroke:#00ff26}.vis.timeline .graphGroup8{fill:#f0f;fill-opacity:0;stroke-width:2px;stroke:#f0f}.vis.timeline .graphGroup9{fill:#8f3938;fill-opacity:0;stroke-width:2px;stroke:#8f3938}.vis.timeline .fill{fill-opacity:.1;stroke:none}.vis.timeline .bar{fill-opacity:.5;stroke-width:1px}.vis.timeline .point{stroke-width:2px;fill-opacity:1}.vis.timeline .legendBackground{stroke-width:1px;fill-opacity:.9;fill:#fff;stroke:#c2c2c2}.vis.timeline .outline{stroke-width:1px;fill-opacity:1;fill:#fff;stroke:#e5e5e5}.vis.timeline .iconFill{fill-opacity:.3;stroke:none}div.network-manipulationDiv{border-width:0;border-bottom:1px;border-style:solid;border-color:#d6d9d8;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(to bottom,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#fcfcfc', GradientType=0);position:absolute;left:0;top:0;width:100%;height:30px}div.network-manipulation-editMode{position:absolute;left:0;top:15px;height:30px}div.network-manipulation-closeDiv{position:absolute;right:0;top:0;width:30px;height:30px;background-position:20px 3px;background-repeat:no-repeat;background-image:url(img/network/cross.png);cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.network-manipulation-closeDiv:hover{opacity:.6}div.network-manipulationUI{position:relative;top:-7px;font-family:verdana;font-size:12px;-moz-border-radius:15px;border-radius:15px;display:inline-block;background-position:0 0;background-repeat:no-repeat;height:24px;margin:0 0 0 10px;vertical-align:middle;cursor:pointer;padding:0 8px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.network-manipulationUI:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}div.network-manipulationUI:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}div.network-manipulationUI.back{background-image:url(img/network/backIcon.png)}div.network-manipulationUI.none:hover{box-shadow:1px 1px 8px transparent;cursor:default}div.network-manipulationUI.none:active{box-shadow:1px 1px 8px transparent}div.network-manipulationUI.none{padding:0}div.network-manipulationUI.notification{margin:2px;font-weight:700}div.network-manipulationUI.add{background-image:url(img/network/addNodeIcon.png)}div.network-manipulationUI.edit{background-image:url(img/network/editIcon.png)}div.network-manipulationUI.edit.editmode{background-color:#fcfcfc;border-style:solid;border-width:1px;border-color:#ccc}div.network-manipulationUI.connect{background-image:url(img/network/connectIcon.png)}div.network-manipulationUI.delete{background-image:url(img/network/deleteIcon.png)}div.network-manipulationLabel{margin:0 0 0 23px;line-height:25px}div.network-seperatorLine{display:inline-block;width:1px;height:20px;background-color:#bdbdbd;margin:5px 7px 0 15px}div.network-navigation_wrapper{position:absolute;left:0;top:0;width:100%;height:100%}div.network-navigation{width:34px;height:34px;-moz-border-radius:17px;border-radius:17px;position:absolute;display:inline-block;background-position:2px 2px;background-repeat:no-repeat;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.network-navigation:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.network-navigation:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.network-navigation.up{background-image:url(img/network/upArrow.png);bottom:50px;left:55px}div.network-navigation.down{background-image:url(img/network/downArrow.png);bottom:10px;left:55px}div.network-navigation.left{background-image:url(img/network/leftArrow.png);bottom:10px;left:15px}div.network-navigation.right{background-image:url(img/network/rightArrow.png);bottom:10px;left:95px}div.network-navigation.zoomIn{background-image:url(img/network/plus.png);bottom:10px;right:15px}div.network-navigation.zoomOut{background-image:url(img/network/minus.png);bottom:10px;right:55px}div.network-navigation.zoomExtends{background-image:url(img/network/zoomExtends.png);bottom:50px;right:15px}div.network-tooltip{position:absolute;visibility:hidden;padding:5px;white-space:nowrap;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border:1px solid;box-shadow:3px 3px 10px rgba(128,128,128,.5)} \ No newline at end of file diff --git a/docs/timeline.html b/docs/timeline.html index cc07b99a..26fcefd9 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -1401,10 +1401,10 @@ To load a locale into the Timeline not supported by default, one can add a new l Current datetoday, tomorrow, yesterday, current-week, current-month, current-year - Hours0h, 1h, ..., 23h + Hoursh0, h1, ..., h23 - Grouped hours0-4h to 20-24h + Grouped hoursh0-h4 to h20-h24 Weekdaymonday, tuesday, wednesday, thursday, friday, saturday, sunday diff --git a/examples/graph2d/19_labels.html b/examples/graph2d/19_labels.html index a235a57a..7f140180 100644 --- a/examples/graph2d/19_labels.html +++ b/examples/graph2d/19_labels.html @@ -57,6 +57,7 @@ var options = { start: '2014-06-10', end: '2014-06-18', + style:'bar' }; var graph2d = new vis.Graph2d(container, dataset, options); diff --git a/examples/network/39_newClustering.html b/examples/network/39_newClustering.html new file mode 100644 index 00000000..c324e8d4 --- /dev/null +++ b/examples/network/39_newClustering.html @@ -0,0 +1,103 @@ + + + + Network | Basic usage + + + + + + + + + +
+ + + + + diff --git a/examples/timeline/18_range_overflow.html b/examples/timeline/18_range_overflow.html index 8f9f7506..d222fa75 100644 --- a/examples/timeline/18_range_overflow.html +++ b/examples/timeline/18_range_overflow.html @@ -11,7 +11,7 @@ font-family: sans-serif; } - .vis.timeline .item.range .content { + .vis.timeline .item.range { overflow: visible; } @@ -22,7 +22,7 @@ In case of ranges being spread over a wide range of time, it can be interesting to have the text contents of the ranges overflow the box. This can be achieved by changing the overflow property of the contents to visible with css:

-.vis.timeline .item.range .content {
+.vis.timeline .item.range {
   overflow: visible;
 }
 
diff --git a/gulpfile.js b/gulpfile.js index 71986d51..176b868e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -34,7 +34,6 @@ var bannerPlugin = new webpack.BannerPlugin(createBanner(), { raw: true }); -// TODO: the moment.js language files should be excluded by default (they are quite big) var webpackConfig = { entry: ENTRY, output: { @@ -44,8 +43,12 @@ var webpackConfig = { filename: VIS_JS, sourcePrefix: ' ' }, - // exclude requires of moment.js language files module: { + loaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: '6to5-loader'} + ], + + // exclude requires of moment.js language files wrappedContextRegExp: /$^/ }, plugins: [ bannerPlugin ], diff --git a/lib/DataSet.js b/lib/DataSet.js index ccb2c2d6..fff1b989 100644 --- a/lib/DataSet.js +++ b/lib/DataSet.js @@ -153,9 +153,7 @@ DataSet.prototype.subscribe = DataSet.prototype.on; DataSet.prototype.off = function(event, callback) { var subscribers = this._subscribers[event]; if (subscribers) { - this._subscribers[event] = subscribers.filter(function (listener) { - return (listener.callback != callback); - }); + this._subscribers[event] = subscribers.filter(listener => listener.callback != callback); } }; diff --git a/lib/network/Edge.js b/lib/network/Edge.js index ccb091c1..b91697d0 100644 --- a/lib/network/Edge.js +++ b/lib/network/Edge.js @@ -16,18 +16,16 @@ var Node = require('./Node'); * @param {Object} constants An object with default values for * example for the color */ -function Edge (properties, network, networkConstants) { - if (!network) { - throw "No network provided"; +function Edge (properties, body, networkConstants) { + if (body === undefined) { + throw "No body provided"; } - var fields = ['edges','physics']; + var fields = ['edges']; var constants = util.selectiveBridgeObject(fields,networkConstants); this.options = constants.edges; - this.physics = constants.physics; - this.options['smoothCurves'] = networkConstants['smoothCurves']; - - this.network = network; + this.options['smoothCurves'] = networkConstants['smoothCurves']; + this.body = body; // initialize variables this.id = undefined; @@ -46,13 +44,13 @@ function Edge (properties, network, networkConstants) { this.to = null; // a node this.via = null; // a temp node - this.fromBackup = null; // used to clean up after reconnect - this.toBackup = null;; // used to clean up after reconnect + this.fromBackup = null; // used to clean up after reconnect (used for manipulation) + this.toBackup = null; // used to clean up after reconnect (used for manipulation) // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster // by storing the original information we can revert to the original connection when the cluser is opened. - this.originalFromId = []; - this.originalToId = []; + this.fromArray = []; + this.toArray = []; this.connected = false; @@ -76,10 +74,11 @@ Edge.prototype.setProperties = function(properties) { if (!properties) { return; } + this.properties = properties; var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width', 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity', - 'customScalingFunction','useGradients' + 'customScalingFunction','useGradients','value' ]; util.selectiveDeepExtend(fields, this.options, properties); @@ -106,9 +105,7 @@ Edge.prototype.setProperties = function(properties) { } } - - - // A node is connected when it has a from and to node. + // A node is connected when it has a from and to node that both exist in the network.body.nodes. this.connect(); this.widthFixed = this.widthFixed || (properties.width !== undefined); @@ -133,11 +130,11 @@ Edge.prototype.setProperties = function(properties) { Edge.prototype.connect = function () { this.disconnect(); - this.from = this.network.nodes[this.fromId] || null; - this.to = this.network.nodes[this.toId] || null; - this.connected = (this.from && this.to); + this.from = this.body.nodes[this.fromId] || null; + this.to = this.body.nodes[this.toId] || null; + this.connected = (this.from !== null && this.to !== null); - if (this.connected) { + if (this.connected === true) { this.from.attachEdge(this); this.to.attachEdge(this); } @@ -259,6 +256,7 @@ Edge.prototype._getColor = function(ctx) { } if (this.colorDirty === true) { + if (this.options.inheritColor == "to") { colorObj = { highlight: this.to.options.color.highlight.border, diff --git a/lib/network/Network.js b/lib/network/Network.js index 7b011750..76d1cb14 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -19,6 +19,13 @@ var locales = require('./locales'); // Load custom shapes into CanvasRenderingContext2D require('./shapes'); +import { PhysicsEngine } from './modules/PhysicsEngine' +import { ClusterEngine } from './modules/Clustering' +import { CanvasRenderer } from './modules/CanvasRenderer' +import { Canvas } from './modules/Canvas' +import { View } from './modules/View' +import { TouchEventHandler } from './modules/TouchEventHandler' + /** * @constructor Network * Create a network visualization, displaying nodes and edges. @@ -35,20 +42,10 @@ function Network (container, data, options) { throw new SyntaxError('Constructor must be called with the new operator'); } - this._determineBrowserMethod(); this._initializeMixinLoaders(); - // create variables and set default values - this.containerElement = container; // render and calculation settings - this.renderRefreshRate = 60; // hz (fps) - this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on - this.renderTime = 0; // measured time it takes to render a frame - this.physicsTime = 0; // measured time it takes to render a frame - this.runDoubleSpeed = false; - this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation - this.initializing = true; this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; @@ -62,6 +59,7 @@ function Network (container, data, options) { return Math.max(0,(value - min)*scale); } }; + // set constant values this.defaultOptions = { nodes: { @@ -85,6 +83,7 @@ function Network (container, data, options) { fontSizeMin: 14, fontSizeMax: 30, fontSizeMaxVisible: 30, + value: 1, level: -1, color: { border: '#2B7CE9', @@ -109,6 +108,7 @@ function Network (container, data, options) { width: 1, widthSelectionMultiplier: 2, hoverWidth: 1.5, + value:1, style: 'line', color: { color:'#848484', @@ -133,58 +133,6 @@ function Network (container, data, options) { useGradients: false // release in 4.0 }, configurePhysics:false, - physics: { - barnesHut: { - enabled: true, - thetaInverted: 1 / 0.5, // inverted to save time during calculation - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0.0, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - enabled: false, - centralGravity: 0.0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 150, - damping: 0.09 - }, - damping: null, - centralGravity: null, - springLength: null, - springConstant: null - }, - clustering: { // Per Node in Cluster = PNiC - enabled: false, // (Boolean) | global on/off switch for clustering. - initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. - clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes - reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this - chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). - clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. - sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. - screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. - fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). - maxFontSize: 1000, - forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). - distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). - edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. - nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster. - height: 1, // (px PNiC) | growth of the height per node in cluster. - radius: 1}, // (px PNiC) | growth of the radius per node in cluster. - maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster. - activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open. - clusterLevelDifference: 2, // used for normalization of the cluster levels - clusterByZoom: true // enable clustering through zooming in and out - }, navigation: { enabled: false }, @@ -204,18 +152,13 @@ function Network (container, data, options) { direction: "UD", // UD, DU, LR, RL layout: "hubsize" // hubsize, directed }, - freezeForStabilization: false, + smoothCurves: { enabled: true, dynamic: true, type: "continuous", roundness: 0.5 }, - maxVelocity: 50, - minVelocity: 0.1, // px/s - stabilize: true, // stabilize before displaying the network - stabilizationIterations: 1000, // maximum number of iteration to stabilize - zoomExtentOnStabilize: true, locale: 'en', locales: locales, tooltip: { @@ -240,34 +183,67 @@ function Network (container, data, options) { useDefaultGroups: true }; this.constants = util.extend({}, this.defaultOptions); - this.pixelRatio = 1; - - + + // containers for nodes and edges + this.body = { + nodes: {}, + nodeIndices: [], + supportNodes: {}, + supportNodeIndices: [], + edges: {}, + data: { + nodes: null, // A DataSet or DataView + edges: null // A DataSet or DataView + }, + functions:{ + createNode: this._createNode.bind(this), + createEdge: this._createEdge.bind(this) + }, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this), + once: this.once.bind(this) + }, + eventListeners: { + onTap: function() {}, + onTouch: function() {}, + onDoubleTap: function() {}, + onHold: function() {}, + onDragStart: function() {}, + onDrag: function() {}, + onDragEnd: function() {}, + onMouseWheel: function() {}, + onPinch: function() {}, + onMouseMove: function() {}, + onRelease: function() {} + }, + container: container + }; + + // modules + this.view = new View(this.body); + this.renderer = new CanvasRenderer(this.body); + this.clustering = new ClusterEngine(this.body); + this.physics = new PhysicsEngine(this.body); + this.canvas = new Canvas(this.body); + this.touchHandler = new TouchEventHandler(this.body); + + this.renderer.setCanvas(this.canvas); + this.view.setCanvas(this.canvas); + this.touchHandler.setCanvas(this.canvas); + this.hoverObj = {nodes:{},edges:{}}; this.controlNodesActive = false; this.navigationHammers = []; this.manipulationHammers = []; - // animation properties - this.animationSpeed = 1/this.renderRefreshRate; - this.animationEasingFunction = "easeInOutQuint"; - this.animating = false; - this.easingTime = 0; - this.sourceScale = 0; - this.targetScale = 0; - this.sourceTranslation = 0; - this.targetTranslation = 0; - this.lockedOnNodeId = null; - this.lockedOnNodeOffset = null; - this.touchTime = 0; - this.redrawRequested = false; - // Node variables - var network = this; + var me = this; this.groups = new Groups(); // object with groups this.images = new Images(); // object with images this.images.setOnloadCallback(function (status) { - network._requestRedraw(); + me._requestRedraw(); }); // keyboard navigation variables @@ -277,90 +253,68 @@ function Network (container, data, options) { // loading all the mixins: // load the force calculation functions, grouped under the physics system. - this._loadPhysicsSystem(); + //this._loadPhysicsSystem(); // create a frame and canvas - this._create(); - // load the sector system. (mandatory, fully integrated with Network) - this._loadSectorSystem(); // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) - this._loadClusterSystem(); // load the selection system. (mandatory, required by Network) this._loadSelectionSystem(); // load the selection system. (mandatory, required by Network) - this._loadHierarchySystem(); - + //this._loadHierarchySystem(); // apply options - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); this.setOptions(options); // other vars - this.freezeSimulationEnabled = false;// freeze the simulation this.cachedFunctions = {}; this.startedStabilization = false; this.stabilized = false; this.stabilizationIterations = null; this.draggingNodes = false; - // containers for nodes and edges - this.calculationNodes = {}; - this.calculationNodeIndices = []; - this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects - // position and scale variables and objects - this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. - this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw + this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action this.scale = 1; // defining the global scale variable in the constructor - this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out - - // datasets or dataviews - this.nodesData = null; // A DataSet or DataView - this.edgesData = null; // A DataSet or DataView // create event listeners used to subscribe on the DataSets of the nodes and edges this.nodesListeners = { 'add': function (event, params) { - network._addNodes(params.items); - network.start(); + me._addNodes(params.items); + me.start(); }, 'update': function (event, params) { - network._updateNodes(params.items, params.data); - network.start(); + me._updateNodes(params.items, params.data); + me.start(); }, 'remove': function (event, params) { - network._removeNodes(params.items); - network.start(); + me._removeNodes(params.items); + me.start(); } }; this.edgesListeners = { 'add': function (event, params) { - network._addEdges(params.items); - network.start(); + me._addEdges(params.items); + me.start(); }, 'update': function (event, params) { - network._updateEdges(params.items); - network.start(); + me._updateEdges(params.items); + me.start(); }, 'remove': function (event, params) { - network._removeEdges(params.items); - network.start(); + me._removeEdges(params.items); + me.start(); } }; + // properties for the animation this.moving = true; - this.timer = undefined; // Scheduling function. Is definded in this.start(); + this.renderTimer = undefined; // Scheduling function. Is definded in this.start(); // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled); + this.setData(data, this.constants.hierarchicalLayout.enabled); // hierarchical layout - this.initializing = false; if (this.constants.hierarchicalLayout.enabled == true) { this._setupHierarchicalLayout(); } @@ -371,224 +325,48 @@ function Network (container, data, options) { } } - // if clustering is disabled, the simulation will have started in the setData function - if (this.constants.clustering.enabled) { - this.startWithClustering(); + if (this.constants.stabilize == false) { + this.initializing = false; } + + var me = this; + // this event will trigger a rebuilding of the cache of colors, nodes etc. + this.on("_dataChanged", function () { + me._updateNodeIndexList(); + me.physics._updateCalculationNodes(); + me._markAllEdgesAsDirty(); + if (me.initializing !== true) { + me.moving = true; + me.start(); + } + }) + + this.on("_newEdgesCreated", this._createBezierNodes.bind(this)); + //this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this)); } // Extend Network with an Emitter mixin Emitter(Network.prototype); -/** - * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because - * some implementations (safari and IE9) did not support requestAnimationFrame - * @private - */ -Network.prototype._determineBrowserMethod = function() { - var browserType = navigator.userAgent.toLowerCase(); - this.requiresTimeout = false; - if (browserType.indexOf('msie 9.0') != -1) { // IE 9 - this.requiresTimeout = true; - } - else if (browserType.indexOf('safari') != -1) { // safari - if (browserType.indexOf('chrome') <= -1) { - this.requiresTimeout = true; - } - } -} - - -/** - * 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 - */ -Network.prototype._getScriptPath = function() { - var scripts = document.getElementsByTagName( 'script' ); - - // find script named vis.js or vis.min.js - for (var i = 0; i < scripts.length; i++) { - var src = scripts[i].src; - var match = src && /\/?vis(.min)?\.js$/.exec(src); - if (match) { - // return path without the script name - return src.substring(0, src.length - match[0].length); - } - } - - return null; -}; - - -/** - * Find the center position of the network - * @private - */ -Network.prototype._getRange = function(specificNodes) { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - if (specificNodes.length > 0) { - for (var i = 0; i < specificNodes.length; i++) { - node = this.nodes[specificNodes[i]]; - if (minX > (node.boundingBox.left)) { - minX = node.boundingBox.left; - } - if (maxX < (node.boundingBox.right)) { - maxX = node.boundingBox.right; - } - if (minY > (node.boundingBox.bottom)) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < (node.boundingBox.top)) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive - } - } - else { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (minX > (node.boundingBox.left)) { - minX = node.boundingBox.left; - } - if (maxX < (node.boundingBox.right)) { - maxX = node.boundingBox.right; - } - if (minY > (node.boundingBox.bottom)) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < (node.boundingBox.top)) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive - } - } - } - - if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { - minY = 0, maxY = 0, minX = 0, maxX = 0; - } - return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; -}; - -/** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} - * @private - */ -Network.prototype._findCenter = function(range) { - return {x: (0.5 * (range.maxX + range.minX)), - y: (0.5 * (range.maxY + range.minY))}; -}; - - -/** - * This function zooms out to fit all data on screen based on amount of nodes - * - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - * @param {Boolean} [disableStart] | If true, start is not called. - */ -Network.prototype.zoomExtent = function(options, initialZoom, disableStart) { - this._redraw(true); - - if (initialZoom === undefined) {initialZoom = false;} - if (disableStart === undefined) {disableStart = false;} - if (options === undefined) {options = {nodes:[]};} - if (options.nodes === undefined) { - options.nodes = []; - } - - var range; - var zoomLevel; - - if (initialZoom == true) { - // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. - var positionDefined = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.predefinedPosition == true) { - positionDefined += 1; - } - } - } - if (positionDefined > 0.5 * this.nodeIndices.length) { - this.zoomExtent(options,false,disableStart); - return; - } - - range = this._getRange(options.nodes); - - var numberOfNodes = this.nodeIndices.length; - if (this.constants.smoothCurves == true) { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - } - else { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - } - - // correct for larger canvasses. - var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600); - zoomLevel *= factor; - } - else { - range = this._getRange(options.nodes); - var xDistance = Math.abs(range.maxX - range.minX) * 1.1; - var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - - var xZoomLevel = this.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.frame.canvas.clientHeight / yDistance; - zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; - } +Network.prototype._createNode = function(properties) { + return new Node(properties, this.images, this.groups, this.constants) +} - if (zoomLevel > 1.0) { - zoomLevel = 1.0; - } +Network.prototype._createEdge = function(properties) { + return new Edge(properties, this.body, this.constants) +} - var center = this._findCenter(range); - if (disableStart == false) { - var options = {position: center, scale: zoomLevel, animation: options}; - this.moveTo(options); - this.moving = true; - this.start(); - } - else { - center.x *= zoomLevel; - center.y *= zoomLevel; - center.x -= 0.5 * this.frame.canvas.clientWidth; - center.y -= 0.5 * this.frame.canvas.clientHeight; - this._setScale(zoomLevel); - this._setTranslation(-center.x,-center.y); - } -}; /** - * Update the this.nodeIndices with the most recent node index list + * Update the this.body.nodeIndices with the most recent node index list * @private */ Network.prototype._updateNodeIndexList = function() { - this._clearNodeIndexList(); - for (var idx in this.nodes) { - if (this.nodes.hasOwnProperty(idx)) { - this.nodeIndices.push(idx); - } - } + this.body.supportNodeIndices = Object.keys(this.body.supportNodes) + this.body.nodeIndices = Object.keys(this.body.nodes); }; @@ -647,7 +425,7 @@ Network.prototype.setData = function(data, disableStart) { this._setNodes(data && data.nodes); this._setEdges(data && data.edges); } - this._putDataInSector(); + if (disableStart == false) { if (this.constants.hierarchicalLayout.enabled == true) { this._resetLevels(); @@ -655,13 +433,12 @@ Network.prototype.setData = function(data, disableStart) { } else { // find a stable position or start animating to a stable position - if (this.constants.stabilize == true) { - this._stabilize(); - } + this.physics.startSimulation() } - this.start(); } - this.initializing = false; + else { + this.initializing = false; + } }; /** @@ -671,7 +448,7 @@ Network.prototype.setData = function(data, disableStart) { Network.prototype.setOptions = function (options) { if (options) { var prop; - var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation', + var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation', 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse' ]; // extend all but the values in fields @@ -680,27 +457,16 @@ Network.prototype.setOptions = function (options) { util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); this.groups.useDefaultGroups = this.constants.useDefaultGroups; - if (options.physics) { - util.mergeOptions(this.constants.physics, options.physics,'barnesHut'); - util.mergeOptions(this.constants.physics, options.physics,'repulsion'); - - if (options.physics.hierarchicalRepulsion) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - for (prop in options.physics.hierarchicalRepulsion) { - if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) { - this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop]; - } - } - } - } - if (options.onAdd) {this.triggerFunctions.add = options.onAdd;} - if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;} + this.physics.setOptions(options.physics); + this.canvas.setOptions(this.constants); + + + if (options.onAdd) {this.triggerFunctions.add = options.onAdd;} + if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;} if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;} - if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;} - if (options.onDelete) {this.triggerFunctions.del = options.onDelete;} + if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;} + if (options.onDelete) {this.triggerFunctions.del = options.onDelete;} util.mergeOptions(this.constants, options,'smoothCurves'); util.mergeOptions(this.constants, options,'hierarchicalLayout'); @@ -790,175 +556,89 @@ Network.prototype.setOptions = function (options) { throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.'); } - // (Re)loading the mixins that can be enabled or disabled in the options. // load the force calculation functions, grouped under the physics system. - this._loadPhysicsSystem(); // load the navigation system. - this._loadNavigationControls(); - // load the data manipulation system - this._loadManipulationSystem(); - // configure the smooth curves - this._configureSmoothCurves(); + //this._loadNavigationControls(); + //// load the data manipulation system + //this._loadManipulationSystem(); + //// configure the smooth curves + //this._configureSmoothCurves(); // bind hammer - this._bindHammer(); + this.canvas._bindHammer(); // bind keys. If disabled, this will not do anything; - this._createKeyBinds(); + //this._createKeyBinds(); this._markAllEdgesAsDirty(); - this.setSize(this.constants.width, this.constants.height); - this.moving = true; + this.canvas.setSize(this.constants.width, this.constants.height); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } - this.start(); - } -}; - - - -/** - * Create the main frame for the Network. - * This function is executed once when a Network object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - * @private - */ -Network.prototype._create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } - - this.frame = document.createElement('div'); - this.frame.className = 'vis network-frame'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; - this.frame.tabIndex = 900; - - -////////////////////////////////////////////////////////////////// - - this.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); - - if (!this.frame.canvas.getContext) { - var noCanvas = document.createElement( 'DIV' ); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold' ; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); - } - else { - var ctx = this.frame.canvas.getContext("2d"); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || - ctx.mozBackingStorePixelRatio || - ctx.msBackingStorePixelRatio || - ctx.oBackingStorePixelRatio || - ctx.backingStorePixelRatio || 1); - //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + if (this.initializing !== true) { + this.moving = true; + this.start(); + } } - - this._bindHammer(); }; -/** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. - * @private - */ -Network.prototype._bindHammer = function() { - var me = this; - if (this.hammer !== undefined) { - this.hammer.dispose(); - } - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on('tap', me._onTap.bind(me) ); - this.hammer.on('doubletap', me._onDoubleTap.bind(me) ); - this.hammer.on('hold', me._onHold.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) ); - - if (this.constants.zoomable == true) { - this.hammer.on('mousewheel', me._onMouseWheel.bind(me)); - this.hammer.on('DOMMouseScroll', me._onMouseWheel.bind(me)); // for FF - this.hammer.on('pinch', me._onPinch.bind(me) ); - } - - this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); - - this.hammerFrame = Hammer(this.frame, { - prevent_default: true - }); - this.hammerFrame.on('release', me._onRelease.bind(me) ); - - // 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 */ Network.prototype._createKeyBinds = function() { - var me = this; - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } - - if (this.constants.keyboard.bindToWindow == true) { - this.keycharm = keycharm({container: window, preventDefault: false}); - } - else { - this.keycharm = keycharm({container: this.frame, preventDefault: false}); - } - - this.keycharm.reset(); - - if (this.constants.keyboard.enabled && this.isActive()) { - this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); - this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); - this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); - this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); - this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); - this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); - this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); - this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); - this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); - this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); - this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); - this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); - this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); - this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); - this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); - } - - if (this.constants.dataManipulation.enabled == true) { - this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); - this.keycharm.bind("delete",this._deleteSelected.bind(me)); - } + return; + + //var me = this; + //if (this.keycharm !== undefined) { + // this.keycharm.destroy(); + //} + // + //if (this.constants.keyboard.bindToWindow == true) { + // this.keycharm = keycharm({container: window, preventDefault: false}); + //} + //else { + // this.keycharm = keycharm({container: this.frame, preventDefault: false}); + //} + // + //this.keycharm.reset(); + // + //if (this.constants.keyboard.enabled && this.isActive()) { + // this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); + // this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); + // this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); + // this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); + // this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); + // this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); + // this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); + // this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); + // this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); + // this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); + //} + // + //if (this.constants.dataManipulation.enabled == true) { + // this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); + // this.keycharm.bind("delete",this._deleteSelected.bind(me)); + //} }; /** @@ -970,7 +650,7 @@ Network.prototype._createKeyBinds = function() { Network.prototype.destroy = function() { this.start = function () {}; this.redraw = function () {}; - this.timer = false; + this.renderTimer = false; // cleanup physicsConfiguration if it exists this._cleanupPhysicsConfiguration(); @@ -995,529 +675,91 @@ Network.prototype._recursiveDOMDelete = function(DOMobject) { } /** - * Get the pointer location from a touch location - * @param {{pageX: Number, pageY: Number}} touch - * @return {{x: Number, y: Number}} pointer + * Check if there is an element on the given position in the network + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer * @private */ -Network.prototype._getPointer = function (touch) { - return { - x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas), - y: touch.pageY - util.getAbsoluteTop(this.frame.canvas) +Network.prototype._checkShowPopup = function (pointer) { + var obj = { + left: this._XconvertDOMtoCanvas(pointer.x), + top: this._YconvertDOMtoCanvas(pointer.y), + right: this._XconvertDOMtoCanvas(pointer.x), + bottom: this._YconvertDOMtoCanvas(pointer.y) }; -}; -/** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ -Network.prototype._onTouch = function (event) { - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this._getPointer(event.gesture.center); - this.drag.pinched = false; - this.pinch.scale = this._getScale(); + var id; + var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = "node"; - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + if (this.popupObj == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.body.nodes; + var overlappingNodes = []; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.isOverlappingWith(obj)) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(id); + } + } + } + } - this._handleTouch(this.drag.pointer); + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + this.popupObj = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; + } } -}; - -/** - * handle drag start event - * @private - */ -Network.prototype._onDragStart = function (event) { - this._handleDragStart(event); -}; + if (this.popupObj === undefined && nodeUnderCursor == false) { + // search the edges for overlap + var edges = this.body.edges; + var overlappingEdges = []; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected === true && (edge.getTitle() !== undefined) && + edge.isOverlappingWith(obj)) { + overlappingEdges.push(id); + } + } + } -/** - * This function is called by _onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. - * - * @private - */ -Network.prototype._handleDragStart = function(event) { - // in case the touch event was triggered on an external div, do the initial touch now. - if (this.drag.pointer === undefined) { - this._onTouch(event); + if (overlappingEdges.length > 0) { + this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = "edge"; + } } - var node = this._getNodeAt(this.drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location + if (this.popupObj) { + // show popup message window + if (this.popupObj.id != previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new Popup(this.frame, this.constants.tooltip); + } - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = this._getTranslation(); - this.drag.nodeId = null; - this.draggingNodes = false; + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - if (node != null && this.constants.dragNodes == true) { - this.draggingNodes = true; - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectObject(node,false); + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); } - - this.emit("dragStart",{nodeIds:this.getSelection().nodes}); - - // create an array with the selected nodes and their original location and status - for (var objectId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(objectId)) { - var object = this.selectionObj.nodes[objectId]; - var s = { - id: object.id, - node: object, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed - }; - - object.xFixed = true; - object.yFixed = true; - - this.drag.selection.push(s); - } - } - } -}; - - -/** - * handle drag event - * @private - */ -Network.prototype._onDrag = function (event) { - this._handleOnDrag(event) -}; - - -/** - * This function is called by _onDrag. - * It is separated out because we can then overload it for the datamanipulation system. - * - * @private - */ -Network.prototype._handleOnDrag = function(event) { - if (this.drag.pinched) { - return; - } - - // remove the focus on node if it is focussed on by the focusOnNode - this.releaseNode(); - - var pointer = this._getPointer(event.gesture.center); - var me = this; - var drag = this.drag; - var selection = drag.selection; - if (selection && selection.length && this.constants.dragNodes == true) { - // calculate delta's and new location - var deltaX = pointer.x - drag.pointer.x; - var deltaY = pointer.y - drag.pointer.y; - - // update position of all selected nodes - selection.forEach(function (s) { - var node = s.node; - - if (!s.xFixed) { - node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); - } - - if (!s.yFixed) { - node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); - } - }); - - - // start _animationStep if not yet running - if (!this.moving) { - this.moving = true; - this.start(); - } - } - else { - // move the network - if (this.constants.dragNetwork == true) { - // if the drag was not started properly because the click started outside the network div, start it now. - if (this.drag.pointer === undefined) { - this._handleDragStart(event); - return; - } - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; - - this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY - ); - this._redraw(); - } - } -}; - -/** - * handle drag start event - * @private - */ -Network.prototype._onDragEnd = function (event) { - this._handleDragEnd(event); -}; - - -Network.prototype._handleDragEnd = function(event) { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - this.moving = true; - this.start(); - } - else { - this._redraw(); - } - if (this.draggingNodes == false) { - this.emit("dragEnd",{nodeIds:[]}); - } - else { - this.emit("dragEnd",{nodeIds:this.getSelection().nodes}); - } - -} -/** - * handle tap/click event: select/unselect a node - * @private - */ -Network.prototype._onTap = function (event) { - var pointer = this._getPointer(event.gesture.center); - this.pointerPosition = pointer; - this._handleTap(pointer); - -}; - - -/** - * handle doubletap event - * @private - */ -Network.prototype._onDoubleTap = function (event) { - var pointer = this._getPointer(event.gesture.center); - this._handleDoubleTap(pointer); -}; - - -/** - * handle long tap event: multi select nodes - * @private - */ -Network.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.center); - this.pointerPosition = pointer; - this._handleOnHold(pointer); -}; - -/** - * handle the release of the screen - * - * @private - */ -Network.prototype._onRelease = function (event) { - var pointer = this._getPointer(event.gesture.center); - this._handleOnRelease(pointer); -}; - -/** - * Handle pinch event - * @param event - * @private - */ -Network.prototype._onPinch = function (event) { - var pointer = this._getPointer(event.gesture.center); - - this.drag.pinched = true; - if (!('scale' in this.pinch)) { - this.pinch.scale = 1; - } - - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this._zoom(scale, pointer) -}; - -/** - * Zoom the network in or out - * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer Position on screen - * @return {Number} appliedScale scale is limited within the boundaries - * @private - */ -Network.prototype._zoom = function(scale, pointer) { - if (this.constants.zoomable == true) { - var scaleOld = this._getScale(); - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } - - var preScaleDragPointer = null; - if (this.drag !== undefined) { - if (this.drag.dragging == true) { - preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer); - } - } - // + this.frame.canvas.clientHeight / 2 - var translation = this._getTranslation(); - - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; - - this._setScale(scale); - this._setTranslation(tx, ty); - this.updateClustersDefault(); - - if (preScaleDragPointer != null) { - var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } - - this._redraw(); - - if (scaleOld < scale) { - this.emit("zoom", {direction:"+"}); - } - else { - this.emit("zoom", {direction:"-"}); - } - - return scale; - } -}; - - -/** - * 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 - */ -Network.prototype._onMouseWheel = function(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta/120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail/3; - } - - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - - // calculate the new scale - var scale = this._getScale(); - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= (1 + zoom); - - // calculate the pointer location - var gesture = hammerUtil.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); - - // apply the new scale - this._zoom(scale, pointer); - } - - // Prevent default actions caused by mouse wheel. - event.preventDefault(); -}; - - -/** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ -Network.prototype._onMouseMoveTitle = function (event) { - var gesture = hammerUtil.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); - var popupVisible = false; - - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); - } - - // if the popup was not hidden above - if (this.popup.hidden === false) { - popupVisible = true; - this.popup.setPosition(pointer.x + 3,pointer.y - 5) - this.popup.show(); - } - } - - // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over - if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { - this.frame.focus(); - } - - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - var me = this; - var checkShow = function () { - me._checkShowPopup(pointer); - }; - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running calculationTimer - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); - } - } - - /** - * Adding hover highlights - */ - if (this.constants.hover == true) { - // removing all hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } - } - - // adding hover highlights - var obj = this._getNodeAt(pointer); - if (obj == null) { - obj = this._getEdgeAt(pointer); - } - if (obj != null) { - this._hoverObject(obj); - } - - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - this._blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.redraw(); - } -}; - -/** - * Check if there is an element on the given position in the network - * (a node or edge). If so, and if this element has a title, - * show a popup window with its title. - * - * @param {{x:Number, y:Number}} pointer - * @private - */ -Network.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._XconvertDOMtoCanvas(pointer.x), - top: this._YconvertDOMtoCanvas(pointer.y), - right: this._XconvertDOMtoCanvas(pointer.x), - bottom: this._YconvertDOMtoCanvas(pointer.y) - }; - - var id; - var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = "node"; - - if (this.popupObj == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.nodes; - var overlappingNodes = []; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.isOverlappingWith(obj)) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(id); - } - } - } - } - - if (overlappingNodes.length > 0) { - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - this.popupObj = this.nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = true; - } - } - - if (this.popupObj === undefined && nodeUnderCursor == false) { - // search the edges for overlap - var edges = this.edges; - var overlappingEdges = []; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected && (edge.getTitle() !== undefined) && - edge.isOverlappingWith(obj)) { - overlappingEdges.push(id); - } - } - } - - if (overlappingEdges.length > 0) { - this.popupObj = this.edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = "edge"; - } - } - - if (this.popupObj) { - // show popup message window - if (this.popupObj.id != previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new Popup(this.frame, this.constants.tooltip); - } - - this.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; - - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.setText(this.popupObj.getTitle()); - this.popup.show(); - } - } - else { - if (this.popup) { - this.popup.hide(); + } + else { + if (this.popup) { + this.popup.hide(); } } }; @@ -1539,7 +781,7 @@ Network.prototype._checkHidePopup = function (pointer) { var stillOnObj = false; if (this.popup.popupTargetType == 'node') { - stillOnObj = this.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); if (stillOnObj === true) { var overNode = this._getNodeAt(pointer); stillOnObj = overNode.id == this.popup.popupTargetId; @@ -1547,7 +789,7 @@ Network.prototype._checkHidePopup = function (pointer) { } else { if (this._getNodeAt(pointer) === null) { - stillOnObj = this.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); } } @@ -1559,68 +801,23 @@ Network.prototype._checkHidePopup = function (pointer) { }; -/** - * Set a new size for the network - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') - */ -Network.prototype.setSize = function(width, height) { - var emitEvent = false; - var oldWidth = this.frame.canvas.width; - var oldHeight = this.frame.canvas.height; - if (width != this.constants.width || height != this.constants.height || this.frame.style.width != width || this.frame.style.height != height) { - this.frame.style.width = width; - this.frame.style.height = height; - - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; - - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - - this.constants.width = width; - this.constants.height = height; - - emitEvent = true; - } - else { - // this would adapt the width of the canvas to the width from 100% if and only if - // there is a change. - - if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - emitEvent = true; - } - if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - emitEvent = true; - } - } - - if (emitEvent == true) { - this.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio}); - } -}; - /** * Set a data set with nodes for the network * @param {Array | DataSet | DataView} nodes The data containing the nodes. * @private */ Network.prototype._setNodes = function(nodes) { - var oldNodesData = this.nodesData; + var oldNodesData = this.body.data.nodes; if (nodes instanceof DataSet || nodes instanceof DataView) { - this.nodesData = nodes; + this.body.data.nodes = nodes; } else if (Array.isArray(nodes)) { - this.nodesData = new DataSet(); - this.nodesData.add(nodes); + this.body.data.nodes = new DataSet(); + this.body.data.nodes.add(nodes); } else if (!nodes) { - this.nodesData = new DataSet(); + this.body.data.nodes = new DataSet(); } else { throw new TypeError('Array or DataSet expected'); @@ -1634,17 +831,17 @@ Network.prototype._setNodes = function(nodes) { } // remove drawn nodes - this.nodes = {}; + this.body.nodes = {}; - if (this.nodesData) { + if (this.body.data.nodes) { // subscribe to new dataset var me = this; util.forEach(this.nodesListeners, function (callback, event) { - me.nodesData.on(event, callback); + me.body.data.nodes.on(event, callback); }); // draw all new nodes - var ids = this.nodesData.getIds(); + var ids = this.body.data.nodes.getIds(); this._addNodes(ids); } this._updateSelection(); @@ -1659,9 +856,9 @@ Network.prototype._addNodes = function(ids) { var id; for (var i = 0, len = ids.length; i < len; i++) { id = ids[i]; - var data = this.nodesData.get(id); + var data = this.body.data.nodes.get(id); var node = new Node(data, this.images, this.groups, this.constants); - this.nodes[id] = node; // note: this may replace an existing node + this.body.nodes[id] = node; // note: this may replace an existing node if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { var radius = 10 * 0.1*ids.length + 10; var angle = 2 * Math.PI * Math.random(); @@ -1676,10 +873,9 @@ Network.prototype._addNodes = function(ids) { this._resetLevels(); this._setupHierarchicalLayout(); } - this._updateCalculationNodes(); + this.physics._updateCalculationNodes(); this._reconnectEdges(); - this._updateValueRange(this.nodes); - this.updateLabels(); + this._updateValueRange(this.body.nodes); }; /** @@ -1688,7 +884,7 @@ Network.prototype._addNodes = function(ids) { * @private */ Network.prototype._updateNodes = function(ids,changedData) { - var nodes = this.nodes; + var nodes = this.body.nodes; for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; var node = nodes[id]; @@ -1715,8 +911,8 @@ Network.prototype._updateNodes = function(ids,changedData) { Network.prototype._markAllEdgesAsDirty = function() { - for (var edgeId in this.edges) { - this.edges[edgeId].colorDirty = true; + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].colorDirty = true; } } @@ -1726,13 +922,13 @@ Network.prototype._markAllEdgesAsDirty = function() { * @private */ Network.prototype._removeNodes = function(ids) { - var nodes = this.nodes; + var nodes = this.body.nodes; // remove from selection for (var i = 0, len = ids.length; i < len; i++) { if (this.selectionObj.nodes[ids[i]] !== undefined) { - this.nodes[ids[i]].unselect(); - this._removeFromSelection(this.nodes[ids[i]]); + this.body.nodes[ids[i]].unselect(); + this._removeFromSelection(this.body.nodes[ids[i]]); } } @@ -1748,7 +944,7 @@ Network.prototype._removeNodes = function(ids) { this._resetLevels(); this._setupHierarchicalLayout(); } - this._updateCalculationNodes(); + this.physics._updateCalculationNodes(); this._reconnectEdges(); this._updateSelection(); this._updateValueRange(nodes); @@ -1761,17 +957,17 @@ Network.prototype._removeNodes = function(ids) { * @private */ Network.prototype._setEdges = function(edges) { - var oldEdgesData = this.edgesData; + var oldEdgesData = this.body.data.edges; if (edges instanceof DataSet || edges instanceof DataView) { - this.edgesData = edges; + this.body.data.edges = edges; } else if (Array.isArray(edges)) { - this.edgesData = new DataSet(); - this.edgesData.add(edges); + this.body.data.edges = new DataSet(); + this.body.data.edges.add(edges); } else if (!edges) { - this.edgesData = new DataSet(); + this.body.data.edges = new DataSet(); } else { throw new TypeError('Array or DataSet expected'); @@ -1785,17 +981,17 @@ Network.prototype._setEdges = function(edges) { } // remove drawn edges - this.edges = {}; + this.body.edges = {}; - if (this.edgesData) { + if (this.body.data.edges) { // subscribe to new dataset var me = this; util.forEach(this.edgesListeners, function (callback, event) { - me.edgesData.on(event, callback); + me.body.data.edges.on(event, callback); }); // draw all new nodes - var ids = this.edgesData.getIds(); + var ids = this.body.data.edges.getIds(); this._addEdges(ids); } @@ -1808,8 +1004,8 @@ Network.prototype._setEdges = function(edges) { * @private */ Network.prototype._addEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; + var edges = this.body.edges, + edgesData = this.body.data.edges; for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; @@ -1820,12 +1016,12 @@ Network.prototype._addEdges = function (ids) { } var data = edgesData.get(id, {"showInternalIds" : true}); - edges[id] = new Edge(data, this, this.constants); + edges[id] = new Edge(data, this.body, this.constants); } this.moving = true; this._updateValueRange(edges); this._createBezierNodes(); - this._updateCalculationNodes(); + this.physics._updateCalculationNodes(); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); @@ -1838,8 +1034,8 @@ Network.prototype._addEdges = function (ids) { * @private */ Network.prototype._updateEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; + var edges = this.body.edges; + var edgesData = this.body.data.edges; for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; @@ -1848,13 +1044,13 @@ Network.prototype._updateEdges = function (ids) { if (edge) { // update edge edge.disconnect(); - edge.setProperties(data, this.constants); + edge.setProperties(data); edge.connect(); } else { // create edge - edge = new Edge(data, this, this.constants); - this.edges[id] = edge; + edge = new Edge(data, this.body, this.constants); + this.body.edges[id] = edge; } } @@ -1873,7 +1069,7 @@ Network.prototype._updateEdges = function (ids) { * @private */ Network.prototype._removeEdges = function (ids) { - var edges = this.edges; + var edges = this.body.edges; // remove from selection for (var i = 0, len = ids.length; i < len; i++) { @@ -1888,7 +1084,7 @@ Network.prototype._removeEdges = function (ids) { var edge = edges[id]; if (edge) { if (edge.via != null) { - delete this.sectors['support']['nodes'][edge.via.id]; + delete this.body.supportNodes[edge.via.id]; } edge.disconnect(); delete edges[id]; @@ -1901,7 +1097,7 @@ Network.prototype._removeEdges = function (ids) { this._resetLevels(); this._setupHierarchicalLayout(); } - this._updateCalculationNodes(); + this.physics._updateCalculationNodes(); }; /** @@ -1910,12 +1106,11 @@ Network.prototype._removeEdges = function (ids) { */ Network.prototype._reconnectEdges = function() { var id, - nodes = this.nodes, - edges = this.edges; + nodes = this.body.nodes, + edges = this.body.edges; for (id in nodes) { if (nodes.hasOwnProperty(id)) { nodes[id].edges = []; - nodes[id].dynamicEdges = []; } } @@ -1965,92 +1160,11 @@ Network.prototype._updateValueRange = function(obj) { } }; -/** - * Redraw the network with the current data - * chart will be resized too. - */ -Network.prototype.redraw = function() { - this.setSize(this.constants.width, this.constants.height); - this._redraw(); -}; /** - * Redraw the network with the current data - * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. - * @private - */ -Network.prototype._requestRedraw = function(hidden) { - if (this.redrawRequested !== true) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, hidden),0); - } - else { - window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); - } - } -}; - -Network.prototype._redraw = function(hidden, requested) { - if (hidden === undefined) { - hidden = false; - } - this.redrawRequested = false; - var ctx = this.frame.canvas.getContext('2d'); - - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - - // clear the canvas - var w = this.frame.canvas.clientWidth; - var h = this.frame.canvas.clientHeight; - 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._XconvertDOMtoCanvas(0), - "y": this._YconvertDOMtoCanvas(0) - }; - this.canvasBottomRight = { - "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth), - "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight) - }; - - if (hidden === false) { - this._doInAllSectors("_drawAllSectorNodes", ctx); - if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { - this._doInAllSectors("_drawEdges", ctx); - } - } - - if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { - this._doInAllSectors("_drawNodes",ctx,false); - } - - if (hidden === false) { - if (this.controlNodesActive == true) { - this._doInAllSectors("_drawControlNodes", ctx); - } - } - -// this._doInSupportSector("_drawNodes",ctx,true); -// this._drawTree(ctx,"#F00F0F"); - - // restore original scaling and translation - ctx.restore(); - - if (hidden === true) { - ctx.clearRect(0, 0, w, h); - } -} - -/** - * Set the translation of the network - * @param {Number} offsetX Horizontal offset - * @param {Number} offsetY Vertical offset + * Set the translation of the network + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset * @private */ Network.prototype._setTranslation = function(offsetX, offsetY) { @@ -2101,418 +1215,6 @@ Network.prototype._getScale = function() { return this.scale; }; -/** - * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} x - * @returns {number} - * @private - */ -Network.prototype._XconvertDOMtoCanvas = function(x) { - return (x - this.translation.x) / this.scale; -}; - -/** - * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the X coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} x - * @returns {number} - * @private - */ -Network.prototype._XconvertCanvasToDOM = function(x) { - return x * this.scale + this.translation.x; -}; - -/** - * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} y - * @returns {number} - * @private - */ -Network.prototype._YconvertDOMtoCanvas = function(y) { - return (y - this.translation.y) / this.scale; -}; - -/** - * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} y - * @returns {number} - * @private - */ -Network.prototype._YconvertCanvasToDOM = function(y) { - return y * this.scale + this.translation.y ; -}; - - -/** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ -Network.prototype.canvasToDOM = function (pos) { - return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)}; -}; - -/** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ -Network.prototype.DOMtoCanvas = function (pos) { - return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)}; -}; - -/** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] - * @private - */ -Network.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 - */ -Network.prototype._drawEdges = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.setScale(this.scale); - if (edge.connected) { - edges[id].draw(ctx); - } - } - } -}; - -/** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ -Network.prototype._drawControlNodes = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - edges[id]._drawControlNodes(ctx); - } - } -}; - -/** - * Find a stable position for all nodes - * @private - */ -Network.prototype._stabilize = function() { - if (this.constants.freezeForStabilization == true) { - this._freezeDefinedNodes(); - } - - // find stable position - var count = 0; - while (this.moving && count < this.constants.stabilizationIterations) { - this._physicsTick(); - count++; - } - - - if (this.constants.zoomExtentOnStabilize == true) { - this.zoomExtent({duration:0}, false, true); - } - - if (this.constants.freezeForStabilization == true) { - this._restoreFrozenNodes(); - } - - this.emit("stabilizationIterationsDone"); -}; - -/** - * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization - * because only the supportnodes for the smoothCurves have to settle. - * - * @private - */ -Network.prototype._freezeDefinedNodes = function() { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x != null && nodes[id].y != null) { - nodes[id].fixedData.x = nodes[id].xFixed; - nodes[id].fixedData.y = nodes[id].yFixed; - nodes[id].xFixed = true; - nodes[id].yFixed = true; - } - } - } -}; - -/** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private - */ -Network.prototype._restoreFrozenNodes = function() { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].fixedData.x != null) { - nodes[id].xFixed = nodes[id].fixedData.x; - nodes[id].yFixed = nodes[id].fixedData.y; - } - } - } -}; - - -/** - * Check if any of the nodes is still moving - * @param {number} vmin the minimum velocity considered as 'moving' - * @return {boolean} true if moving, false if non of the nodes is moving - * @private - */ -Network.prototype._isMoving = function(vmin) { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes[id] !== undefined) { - if (nodes[id].isMoving(vmin) == true) { - return true; - } - } - } - return false; -}; - - -/** - * /** - * Perform one discrete step for all nodes - * - * @private - */ -Network.prototype._discreteStepNodes = function() { - var interval = this.physicsDiscreteStepsize; - var nodes = this.nodes; - var nodeId; - var nodesPresent = false; - - if (this.constants.maxVelocity > 0) { - for (nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity); - nodesPresent = true; - } - } - } - else { - for (nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].discreteStep(interval); - nodesPresent = true; - } - } - } - - if (nodesPresent == true) { - var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05); - if (vminCorrected > 0.5*this.constants.maxVelocity) { - return true; - } - else { - return this._isMoving(vminCorrected); - } - } - return false; -}; - - -Network.prototype._revertPhysicsState = function() { - var nodes = this.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].revertPosition(); - } - } -} - -Network.prototype._revertPhysicsTick = function() { - this._doInAllActiveSectors("_revertPhysicsState"); - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._doInSupportSector("_revertPhysicsState"); - } -} - -/** - * A single simulation step (or "tick") in the physics simulation - * - * @private - */ -Network.prototype._physicsTick = function() { - if (!this.freezeSimulationEnabled) { - if (this.moving == true) { - var mainMovingStatus = false; - var supportMovingStatus = false; - - this._doInAllActiveSectors("_initializeForceCalculation"); - var mainMoving = this._doInAllActiveSectors("_discreteStepNodes"); - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - supportMovingStatus = this._doInSupportSector("_discreteStepNodes"); - } - - // gather movement data from all sectors, if one moves, we are NOT stabilzied - for (var i = 0; i < mainMoving.length; i++) { - mainMovingStatus = mainMoving[i] || mainMovingStatus; - } - - // determine if the network has stabilzied - this.moving = mainMovingStatus || supportMovingStatus; - if (this.moving == false) { - this._revertPhysicsTick(); - } - else { - // this is here to ensure that there is no start event when the network is already stable. - if (this.startedStabilization == false) { - this.emit("startStabilization"); - this.startedStabilization = true; - } - } - - this.stabilizationIterations++; - } - } -}; - - -/** - * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick. - * It reschedules itself at the beginning of the function - * - * @private - */ -Network.prototype._animationStep = function() { - // reset the timer so a new scheduled animation step can be set - this.timer = undefined; - - if (this.requiresTimeout == true) { - // this schedules a new animation step - this.start(); - } - - // handle the keyboad movement - this._handleNavigation(); - - // check if the physics have settled - if (this.moving == true) { - var startTime = Date.now(); - this._physicsTick(); - var physicsTime = Date.now() - startTime; - - // run double speed if it is a little graph - if ((this.renderTimestep - this.renderTime > 2 * physicsTime || this.runDoubleSpeed == true) && this.moving == true) { - this._physicsTick(); - - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - if (this.renderTime != 0) { - this.runDoubleSpeed = true - } - } - } - - var renderStartTime = Date.now(); - this._redraw(); - this.renderTime = Date.now() - renderStartTime; - - if (this.requiresTimeout == false) { - // this schedules a new animation step - this.start(); - } -}; - -if (typeof window !== 'undefined') { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; -} - -/** - * Schedule a animation step with the refreshrate interval. - */ -Network.prototype.start = function() { - if (this.freezeSimulationEnabled == true) { - this.moving = false; - } - if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0 || this.animating == true) { - if (!this.timer) { - if (this.requiresTimeout == true) { - this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function - } - else { - this.timer = window.requestAnimationFrame(this._animationStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function - } - } - } - else { - this._requestRedraw(); - // this check is to ensure that the network does not emit these events if it was already stabilized and setOptions is called (setting moving to true and calling start()) - if (this.stabilizationIterations > 1) { - // trigger the "stabilized" event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - var me = this; - var params = { - iterations: me.stabilizationIterations - }; - this.stabilizationIterations = 0; - this.startedStabilization = false; - setTimeout(function () { - me.emit("stabilized", params); - }, 0); - } - else { - this.stabilizationIterations = 0; - } - } -}; - /** * Move the network according to the keyboard presses. @@ -2556,33 +1258,31 @@ Network.prototype.freezeSimulation = function(freeze) { * @param {boolean} [disableStart] * @private */ -Network.prototype._configureSmoothCurves = function(disableStart) { - if (disableStart === undefined) { - disableStart = true; - } +Network.prototype._configureSmoothCurves = function(disableStart = true) { if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this._createBezierNodes(); // cleanup unused support nodes - for (var nodeId in this.sectors['support']['nodes']) { - if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) { - if (this.edges[this.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) { - delete this.sectors['support']['nodes'][nodeId]; - } + for (let i = 0; i < this.body.supportNodeIndices.length; i++) { + let nodeId = this.body.supportNodeIndices[i]; + // delete support nodes for edges that have been deleted + if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) { + delete this.body.supportNodes[nodeId]; } } } else { // delete the support nodes - this.sectors['support']['nodes'] = {}; - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - this.edges[edgeId].via = null; + this.body.supportNodes = {}; + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId].via = null; } } } - this._updateCalculationNodes(); + this._updateNodeIndexList(); + this.physics._updateCalculationNodes(); if (!disableStart) { this.moving = true; this.start(); @@ -2596,26 +1296,28 @@ Network.prototype._configureSmoothCurves = function(disableStart) { * * @private */ -Network.prototype._createBezierNodes = function() { +Network.prototype._createBezierNodes = function(specificEdges = this.body.edges) { if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - var edge = this.edges[edgeId]; + for (var edgeId in specificEdges) { + if (specificEdges.hasOwnProperty(edgeId)) { + var edge = specificEdges[edgeId]; if (edge.via == null) { var nodeId = "edgeId:".concat(edge.id); - this.sectors['support']['nodes'][nodeId] = new Node( + var node = new Node( {id:nodeId, mass:1, shape:'circle', image:"", internalMultiplier:1 },{},{},this.constants); - edge.via = this.sectors['support']['nodes'][nodeId]; + this.body.supportNodes[nodeId] = node; + edge.via = node; edge.via.parentEdgeId = edge.id; edge.positionBezierNode(); } } } + this._updateNodeIndexList(); } }; @@ -2645,17 +1347,17 @@ Network.prototype.storePosition = function() { */ Network.prototype.storePositions = function() { var dataArray = []; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - var allowedToMoveX = !this.nodes.xFixed; - var allowedToMoveY = !this.nodes.yFixed; - if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + var allowedToMoveX = !this.body.nodes.xFixed; + var allowedToMoveY = !this.body.nodes.yFixed; + if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY}); } } } - this.nodesData.update(dataArray); + this.body.data.nodes.update(dataArray); }; /** @@ -2666,23 +1368,23 @@ Network.prototype.getPositions = function(ids) { if (ids !== undefined) { if (Array.isArray(ids) == true) { for (var i = 0; i < ids.length; i++) { - if (this.nodes[ids[i]] !== undefined) { - var node = this.nodes[ids[i]]; + if (this.body.nodes[ids[i]] !== undefined) { + var node = this.body.nodes[ids[i]]; dataArray[ids[i]] = {x: Math.round(node.x), y: Math.round(node.y)}; } } } else { - if (this.nodes[ids] !== undefined) { - var node = this.nodes[ids]; + if (this.body.nodes[ids] !== undefined) { + var node = this.body.nodes[ids]; dataArray[ids] = {x: Math.round(node.x), y: Math.round(node.y)}; } } } else { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; dataArray[nodeId] = {x: Math.round(node.x), y: Math.round(node.y)}; } } @@ -2691,190 +1393,6 @@ Network.prototype.getPositions = function(ids) { }; - -/** - * Center a node in view. - * - * @param {Number} nodeId - * @param {Number} [options] - */ -Network.prototype.focusOnNode = function (nodeId, options) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (options === undefined) { - options = {}; - } - var nodePosition = {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y}; - options.position = nodePosition; - options.lockedOnNode = nodeId; - - this.moveTo(options) - } - else { - console.log("This nodeId cannot be found."); - } -}; - -/** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.scale = Number // scale to move to - * | options.position = {x:Number, y:Number} // position to move to - * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to - */ -Network.prototype.moveTo = function (options) { - if (options === undefined) { - options = {}; - return; - } - if (options.offset === undefined) {options.offset = {x: 0, y: 0}; } - if (options.offset.x === undefined) {options.offset.x = 0; } - if (options.offset.y === undefined) {options.offset.y = 0; } - if (options.scale === undefined) {options.scale = this._getScale(); } - if (options.position === undefined) {options.position = this._getTranslation();} - if (options.animation === undefined) {options.animation = {duration:0}; } - if (options.animation === false ) {options.animation = {duration:0}; } - if (options.animation === true ) {options.animation = {}; } - if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration - if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function - - this.animateView(options); -}; - -/** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.time = Number // animation time in milliseconds - * | options.scale = Number // scale to animate to - * | options.position = {x:Number, y:Number} // position to animate to - * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, - * // easeInCubic, easeOutCubic, easeInOutCubic, - * // easeInQuart, easeOutQuart, easeInOutQuart, - * // easeInQuint, easeOutQuint, easeInOutQuint - */ -Network.prototype.animateView = function (options) { - if (options === undefined) { - options = {}; - return; - } - - // release if something focussed on the node - this.releaseNode(); - if (options.locked == true) { - this.lockedOnNodeId = options.lockedOnNode; - this.lockedOnNodeOffset = options.offset; - } - - // forcefully complete the old animation if it was still running - if (this.easingTime != 0) { - this._transitionRedraw(1); // by setting easingtime to 1, we finish the animation. - } - - this.sourceScale = this._getScale(); - this.sourceTranslation = this._getTranslation(); - this.targetScale = options.scale; - - // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw - // but at least then we'll have the target transition - this._setScale(this.targetScale); - var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - options.position.x, - y: viewCenter.y - options.position.y - }; - this.targetTranslation = { - x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, - y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y - }; - - // if the time is set to 0, don't do an animation - if (options.animation.duration == 0) { - if (this.lockedOnNodeId != null) { - this._classicRedraw = this._redraw; - this._redraw = this._lockedRedraw; - } - else { - this._setScale(this.targetScale); - this._setTranslation(this.targetTranslation.x, this.targetTranslation.y); - this._redraw(); - } - } - else { - this.animating = true; - this.animationSpeed = 1 / (this.renderRefreshRate * options.animation.duration * 0.001) || 1 / this.renderRefreshRate; - this.animationEasingFunction = options.animation.easingFunction; - this._classicRedraw = this._redraw; - this._redraw = this._transitionRedraw; - this._redraw(); - this.start(); - } -}; - -/** - * used to animate smoothly by hijacking the redraw function. - * @private - */ -Network.prototype._lockedRedraw = function () { - var nodePosition = {x: this.nodes[this.lockedOnNodeId].x, y: this.nodes[this.lockedOnNodeId].y}; - var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - nodePosition.x, - y: viewCenter.y - nodePosition.y - }; - var sourceTranslation = this._getTranslation(); - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y - }; - - this._setTranslation(targetTranslation.x,targetTranslation.y); - this._classicRedraw(); -} - -Network.prototype.releaseNode = function () { - if (this.lockedOnNodeId != null) { - this._redraw = this._classicRedraw; - this.lockedOnNodeId = null; - this.lockedOnNodeOffset = null; - } -} - -/** - * - * @param easingTime - * @private - */ -Network.prototype._transitionRedraw = function (easingTime) { - this.easingTime = easingTime || this.easingTime + this.animationSpeed; - this.easingTime += this.animationSpeed; - - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - - this._setScale(this.sourceScale + (this.targetScale - this.sourceScale) * progress); - this._setTranslation( - this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, - this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress - ); - - this._classicRedraw(); - - // cleanup - if (this.easingTime >= 1.0) { - this.animating = false; - this.easingTime = 0; - if (this.lockedOnNodeId != null) { - this._redraw = this._lockedRedraw; - } - else { - this._redraw = this._classicRedraw; - } - this.emit("animationFinished"); - } -}; - -Network.prototype._classicRedraw = function () { - // placeholder function to be overloaded by animations; -}; - /** * Returns true when the Network is active. * @returns {boolean} @@ -2902,6 +1420,21 @@ Network.prototype.getScale = function () { }; +/** + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ +Network.prototype.isCluster = function(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster; + } + else { + console.log("Node does not exist.") + return false; + } +}; + /** * Returns the scale * @returns {Number} @@ -2912,15 +1445,15 @@ Network.prototype.getCenterCoordinates = function () { Network.prototype.getBoundingBox = function(nodeId) { - if (this.nodes[nodeId] !== undefined) { - return this.nodes[nodeId].boundingBox; + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].boundingBox; } } Network.prototype.getConnectedNodes = function(nodeId) { var nodeList = []; - if (this.nodes[nodeId] !== undefined) { - var node = this.nodes[nodeId]; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; var nodeObj = {nodeId : true}; // used to quickly check if node already exists for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; @@ -2944,8 +1477,8 @@ Network.prototype.getConnectedNodes = function(nodeId) { Network.prototype.getEdgesFromNode = function(nodeId) { var edgesList = []; - if (this.nodes[nodeId] !== undefined) { - var node = this.nodes[nodeId]; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; for (var i = 0; i < node.edges.length; i++) { edgesList.push(node.edges[i].id); } diff --git a/lib/network/Node.js b/lib/network/Node.js index b88ba8e1..820311fd 100644 --- a/lib/network/Node.js +++ b/lib/network/Node.js @@ -33,8 +33,6 @@ function Node(properties, imagelist, grouplist, networkConstants) { this.hover = false; this.edges = []; // all edges connected to this node - this.dynamicEdges = []; - this.reroutedEdges = {}; // set defaults for the properties this.id = undefined; @@ -56,10 +54,6 @@ function Node(properties, imagelist, grouplist, networkConstants) { this.grouplist = grouplist; // physics properties - this.fx = 0.0; // external force x - this.fy = 0.0; // external force y - this.vx = 0.0; // velocity x - this.vy = 0.0; // velocity y this.x = null; this.y = null; this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate @@ -67,52 +61,19 @@ function Node(properties, imagelist, grouplist, networkConstants) { // used for reverting to previous position on stabilization this.previousState = {vx:0,vy:0,x:0,y:0}; - this.damping = networkConstants.physics.damping; // written every time gravity is calculated this.fixedData = {x:null,y:null}; this.setProperties(properties, constants); - // creating the variables for clustering - this.resetCluster(); - this.clusterSession = 0; - this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width; - this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height; - this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius; - this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements; - this.growthIndicator = 0; - // variables to tell the node about the network. this.networkScaleInv = 1; this.networkScale = 1; - this.canvasTopLeft = {"x": -300, "y": -300}; - this.canvasBottomRight = {"x": 300, "y": 300}; + this.canvasTopLeft = {x: -300, y: -300}; + this.canvasBottomRight = {x: 300, y: 300}; this.parentEdgeId = null; } -/** - * Revert the position and velocity of the previous step. - */ -Node.prototype.revertPosition = function() { - this.x = this.previousState.x; - this.y = this.previousState.y; - this.vx = this.previousState.vx; - this.vy = this.previousState.vy; -} - - -/** - * (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 = []; -}; - /** * Attach a edge to the node * @param {Edge} edge @@ -121,9 +82,6 @@ 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); - } }; /** @@ -135,10 +93,6 @@ Node.prototype.detachEdge = function(edge) { if (index != -1) { this.edges.splice(index, 1); } - index = this.dynamicEdges.indexOf(edge); - if (index != -1) { - this.dynamicEdges.splice(index, 1); - } }; @@ -151,10 +105,12 @@ Node.prototype.setProperties = function(properties, constants) { if (!properties) { return; } + this.properties = properties; - var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor', - 'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass','fontDrawThreshold', - 'scaleFontWithValue','fontSizeMaxVisible','customScalingFunction','iconFontFace', 'icon', 'iconColor', 'iconSize' + var fields = ['borderWidth', 'borderWidthSelected', 'shape', 'image', 'brokenImage', 'radius', 'fontColor', + 'fontSize', 'fontFace', 'fontFill', 'fontStrokeWidth', 'fontStrokeColor', 'group', 'mass', 'fontDrawThreshold', + 'scaleFontWithValue', 'fontSizeMaxVisible', 'customScalingFunction', 'iconFontFace', 'icon', 'iconColor', 'iconSize', + 'value' ]; util.selectiveDeepExtend(fields, this.options, properties); @@ -332,99 +288,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) { // TODO: implement calculation of distance to border for all shapes }; -/** - * Set forces acting on the node - * @param {number} fx Force in horizontal direction - * @param {number} fy Force in vertical direction - */ -Node.prototype._setForce = function(fx, fy) { - this.fx = fx; - this.fy = fy; -}; - -/** - * Add forces acting on the node - * @param {number} fx Force in horizontal direction - * @param {number} fy Force in vertical direction - * @private - */ -Node.prototype._addForce = function(fx, fy) { - this.fx += fx; - this.fy += fy; -}; - -/** - * Store the state before the next step - */ -Node.prototype.storeState = function() { - this.previousState.x = this.x; - this.previousState.y = this.y; - this.previousState.vx = this.vx; - this.previousState.vy = this.vy; -} - -/** - * Perform one discrete step for the node - * @param {number} interval Time interval in seconds - */ -Node.prototype.discreteStep = function(interval) { - this.storeState(); - if (!this.xFixed) { - var dx = this.damping * this.vx; // damping force - var ax = (this.fx - dx) / this.options.mass; // acceleration - this.vx += ax * interval; // velocity - this.x += this.vx * interval; // position - } - else { - this.fx = 0; - this.vx = 0; - } - - if (!this.yFixed) { - var dy = this.damping * this.vy; // damping force - var ay = (this.fy - dy) / this.options.mass; // acceleration - this.vy += ay * interval; // velocity - this.y += this.vy * interval; // position - } - else { - this.fy = 0; - this.vy = 0; - } -}; - - - -/** - * Perform one discrete step for the node - * @param {number} interval Time interval in seconds - * @param {number} maxVelocity The speed limit imposed on the velocity - */ -Node.prototype.discreteStepLimited = function(interval, maxVelocity) { - this.storeState(); - if (!this.xFixed) { - var dx = this.damping * this.vx; // damping force - var ax = (this.fx - dx) / this.options.mass; // acceleration - this.vx += ax * interval; // velocity - this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx; - this.x += this.vx * interval; // position - } - else { - this.fx = 0; - this.vx = 0; - } - - if (!this.yFixed) { - var dy = this.damping * this.vy; // damping force - var ay = (this.fy - dy) / this.options.mass; // acceleration - this.vy += ay * interval; // velocity - this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy; - this.y += this.vy * interval; // position - } - else { - this.fy = 0; - this.vy = 0; - } -}; /** * Check if this node has a fixed x and y position @@ -434,17 +297,6 @@ Node.prototype.isFixed = function() { return (this.xFixed && this.yFixed); }; -/** - * Check if this node is moving - * @param {number} vmin the minimum velocity considered as "moving" - * @return {boolean} true if moving, false if it has no velocity - */ -Node.prototype.isMoving = function(vmin) { - var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2)); -// this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2)) - return (velocity > vmin); -}; - /** * check if this node is selecte * @return {boolean} selected True if node is selected, else false @@ -547,29 +399,11 @@ Node.prototype._resizeImage = function (ctx) { } this.width = width; this.height = height; - - this.growthIndicator = 0; - if (this.width > 0 && this.height > 0) { - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - width; - } } }; Node.prototype._drawImageAtPosition = function (ctx) { if (this.imageObj.width != 0 ) { - // draw the shade - if (this.clusterSize > 1) { - var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0); - lineWidth *= this.networkScaleInv; - lineWidth = Math.min(0.2 * this.width,lineWidth); - - 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); @@ -619,12 +453,6 @@ Node.prototype._resizeCircularImage = function (ctx) { var diameter = this.options.radius * 2; this.width = diameter; this.height = diameter; - - // scaling used for clustering - //this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - //this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.options.radius- 0.5*diameter; this._swapToImageResizeWhenImageLoaded = true; } } @@ -678,12 +506,6 @@ Node.prototype._resizeBox = function (ctx) { var textSize = this.getTextSize(ctx); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; - - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.growthIndicator = this.width - (textSize.width + 2 * margin); -// this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - } }; @@ -693,22 +515,11 @@ Node.prototype._drawBox = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; - var clusterLineWidth = 2.5; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth); ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width,ctx.lineWidth); @@ -734,12 +545,6 @@ Node.prototype._resizeDatabase = function (ctx) { var size = textSize.width + 2 * margin; this.width = size; this.height = size; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - size; } }; @@ -748,22 +553,11 @@ Node.prototype._drawDatabase = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; - var clusterLineWidth = 2.5; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - 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 : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth); ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width,ctx.lineWidth); @@ -790,32 +584,16 @@ Node.prototype._resizeCircle = function (ctx) { this.width = diameter; this.height = diameter; - - // scaling used for clustering -// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; -// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.options.radius- 0.5*diameter; } }; Node.prototype._drawRawCircle = function (ctx, x, y, radius) { - var clusterLineWidth = 2.5; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx.circle(x, y, radius+2*ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth); ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width,ctx.lineWidth); @@ -850,12 +628,6 @@ Node.prototype._resizeEllipse = function (ctx) { this.width = this.height; } var defaultSize = this.width; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - defaultSize; } }; @@ -864,22 +636,12 @@ Node.prototype._drawEllipse = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; - var clusterLineWidth = 2.5; var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - 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 : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth); ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width,ctx.lineWidth); @@ -923,12 +685,6 @@ Node.prototype._resizeShape = function (ctx) { var size = 2 * this.options.radius; this.width = size; this.height = size; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - size; } }; @@ -938,7 +694,6 @@ 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 borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; var radiusMultiplier = 2; @@ -953,16 +708,7 @@ Node.prototype._drawShape = function (ctx, shape) { } ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - - ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth); ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width,ctx.lineWidth); @@ -990,12 +736,6 @@ 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 += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - (textSize.width + 2 * margin); } }; @@ -1022,12 +762,6 @@ Node.prototype._resizeIcon = function (ctx) { }; this.width = iconSize.width + 2 * margin; this.height = iconSize.height + 2 * margin; - - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - (iconSize.width + 2 * margin); } }; @@ -1160,13 +894,14 @@ Node.prototype.getTextSize = function(ctx) { width = Math.max(width, ctx.measureText(lines[i]).width); } - return {"width": width, "height": height, lineCount: lines.length}; + return {width: width, height: height, lineCount: lines.length}; } else { - return {"width": 0, "height": 0, lineCount: 0}; + return {width: 0, height: 0, lineCount: 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; @@ -1185,16 +920,6 @@ Node.prototype.inArea = function() { } }; -/** - * 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 network to influence the rendering diff --git a/lib/network/mixins/ClusterMixin.js b/lib/network/mixins/ClusterMixin.js deleted file mode 100644 index f3a33587..00000000 --- a/lib/network/mixins/ClusterMixin.js +++ /dev/null @@ -1,1129 +0,0 @@ -/** - * Creation of the ClusterMixin var. - * - * This contains all the functions the Network object can use to employ clustering - */ - -/** -* This is only called in the constructor of the network object -* -*/ -exports.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.constants.stabilize == true) { - this._stabilize(); - } - this.start(); -}; - -/** - * This function clusters until the initialMaxNodes has been reached - * - * @param {Number} maxNumberOfNodes - * @param {Boolean} reposition - */ -exports.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.0) { - this.forceAggregateHubs(true); - this.normalizeClusterLevels(); - } - else { - this.increaseClusterLevel(); // this also includes a cluster normalization - } - this.forceAggregateHubs(true); - numberOfNodes = this.nodeIndices.length; - level += 1; - } - - // after the clustering we reposition the nodes to reduce the initial chaos - if (level > 0 && reposition == true) { - this.repositionNodes(); - } - this._updateCalculationNodes(); -}; - -/** - * This function can be called to open up a specific cluster. - * It will unpack the cluster back one level. - * - * @param node | Node object: cluster to open. - */ -exports.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 loads a new sector, loads the nodes and edges and nodeIndices of it. - this._addSector(node); - var level = 0; - - // we decluster until we reach a decent number of nodes - while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { - this.decreaseClusterLevel(); - level += 1; - } - - } - else { - this._expandClusterNode(node,false,true); - - // update the index list and labels - this._updateNodeIndexList(); - this._updateCalculationNodes(); - 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 - */ -exports.updateClustersDefault = function() { - if (this.constants.clustering.enabled == true && this.constants.clustering.clusterByZoom == 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. - */ -exports.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. - */ -exports.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 - * @param {Boolean} doNotStart | if true do not call start - * - */ -exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - var detectedZoomingIn = (this.previousScale < this.scale && zoomDirection == 0); - var detectedZoomingOut = (this.previousScale > this.scale && zoomDirection == 0); - - // on zoom out collapse the sector if the scale is at the level the sector was made - if (detectedZoomingOut == true) { - this._collapseSector(); - } - - // check if we zoom in or out - if (detectedZoomingOut == true || 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 (detectedZoomingIn == true || 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._openClusters(recursive, false); - } - } - this._updateNodeIndexList(); - - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs - if (this.nodeIndices.length == amountOfNodes && (detectedZoomingOut == true || zoomDirection == -1)) { - this._aggregateHubs(force); - this._updateNodeIndexList(); - } - - // we now reduce chains. - if (detectedZoomingOut == true || zoomDirection == -1) { // zoom out - this.handleChains(); - this._updateNodeIndexList(); - } - - this.previousScale = this.scale; - - // update labels - 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 clusters have been made, we normalize the cluster level - this.normalizeClusterLevels(); - } - - if (doNotStart == false || doNotStart === undefined) { - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - } - - this._updateCalculationNodes(); -}; - -/** - * This function handles the chains. It is called on every updateClusters(). - */ -exports.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 - */ -exports._aggregateHubs = function(force) { - this._getHubSize(); - this._formClustersByHub(force,false); -}; - - -/** - * This function forces hubs to form. - * - */ -exports.forceAggregateHubs = function(doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - this._aggregateHubs(true); - - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this.updateLabels(); - - this._updateCalculationNodes(); - - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; - } - - if (doNotStart == false || doNotStart === undefined) { - // 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 - */ -exports._openClustersBySize = function() { - if (this.constants.clustering.clusterByZoom == true) { - 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 - */ -exports._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._updateCalculationNodes(); - } -}; - -/** - * 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 - */ -exports._expandClusterNode = function(parentNode, recursive, force, openAll) { - // first check if node is a cluster - if (parentNode.clusterSize > 1) { - if (openAll === undefined) { - openAll = false; - } - // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - - recursive = openAll || 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 - */ -exports._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) { - // unselect all selected items - this._unselectAll(); - - // 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.options.mass -= childNode.options.mass; - parentNode.clusterSize -= childNode.clusterSize; - parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*(parentNode.clusterSize-1)); - - // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); - childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); - - // 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(); - } - - this._repositionBezierNodes(childNode); -// this._repositionBezierNodes(parentNode); - - // remove the clusterSession from the child node - childNode.clusterSession = 0; - - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - - // restart the simulation to reorganise all nodes - this.moving = true; - } - - // check if a further expansion step is possible if recursivity is enabled - if (recursive == true) { - this._expandClusterNode(childNode,recursive,force,openAll); - } -}; - - -/** - * position the bezier nodes at the center of the edges - * - * @param node - * @private - */ -exports._repositionBezierNodes = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - node.dynamicEdges[i].positionBezierNode(); - } -}; - - -/** - * 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 - */ -exports._formClusters = function(force) { - if (force == false) { - if (this.constants.clustering.clusterByZoom == true) { - this._formClustersByZoom(); - } - } - else { - this._forceClustersByZoom(); - } -}; - - -/** - * This function handles the clustering by zooming out, this is based on a minimum edge distance - * - * @private - */ -exports._formClustersByZoom = function() { - var dx,dy,length; - var 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.options.mass > edge.from.options.mass) { - parentNode = edge.to; - childNode = edge.from; - } - - if (childNode.dynamicEdges.length == 1) { - this._addToCluster(parentNode,childNode,false); - } - else if (parentNode.dynamicEdges.length == 1) { - this._addToCluster(childNode,parentNode,false); - } - } - } - } - } - } -}; - -/** - * This function forces the network to cluster all nodes with only one connecting edge to their - * connected node. - * - * @private - */ -exports._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.dynamicEdges.length == 1) { - 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.options.mass > childNode.options.mass) { - this._addToCluster(parentNode,childNode,true); - } - else { - this._addToCluster(childNode,parentNode,true); - } - } - } - } - } -}; - - -/** - * To keep the nodes of roughly equal size we normalize the cluster levels. - * This function clusters a node to its smallest connected neighbour. - * - * @param node - * @private - */ -exports._clusterToSmallestNeighbour = function(node) { - var smallestNeighbour = -1; - var smallestNeighbourNode = null; - for (var i = 0; i < node.dynamicEdges.length; i++) { - if (node.dynamicEdges[i] !== undefined) { - var neighbour = null; - if (node.dynamicEdges[i].fromId != node.id) { - neighbour = node.dynamicEdges[i].from; - } - else if (node.dynamicEdges[i].toId != node.id) { - neighbour = node.dynamicEdges[i].to; - } - - - if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { - smallestNeighbour = neighbour.clusterSessions.length; - smallestNeighbourNode = neighbour; - } - } - } - - if (neighbour != null && this.nodes[neighbour.id] !== undefined) { - this._addToCluster(neighbour, node, 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 - */ -exports._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 - */ -exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { - if (absorptionSizeOffset === undefined) { - absorptionSizeOffset = 0; - } - //this.hubThreshold = 43 - //if (hubNode.dynamicEdgesLength < 0) { - // console.error(hubNode.dynamicEdgesLength, this.hubThreshold, onlyEqual) - //} - // we decide if the node is a hub - if ((hubNode.dynamicEdges.length >= this.hubThreshold && onlyEqual == false) || - (hubNode.dynamicEdges.length == 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 forced, 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) { - var children = []; - var childrenIds = {}; - // we loop over all edges INITIALLY connected to this hub to get a list of the childNodes - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIdarray[j]]; - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - if (childrenIds[childNode.id] === undefined) { - childrenIds[childNode.id] = true; - children.push(childNode); - } - } - - for (j = 0; j < children.length; j++) { - var childNode = children[j]; - // 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); - - } - else { - //console.log("WILL NOT MERGE:",childNode.dynamicEdges.length , (this.hubThreshold + absorptionSizeOffset)) - } - } - - } - } -}; - - - -/** - * 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 - */ -exports._addToCluster = function(parentNode, childNode, force) { - // join child node in the parent node - parentNode.containedNodes[childNode.id] = childNode; - //console.log(parentNode.id, childNode.id) - // 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 - //console.log("COLLECT",parentNode.id, childNode.id, edge.toId, edge.fromId) - this._addToContainedEdges(parentNode,childNode,edge); - } - else { - //console.log("REWIRE",parentNode.id, childNode.id, edge.toId, edge.fromId) - 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.options.mass; - childNode.clusterSession = this.clusterSession; - parentNode.options.mass += childNode.options.mass; - parentNode.clusterSize += childNode.clusterSize; - parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.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 = 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 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 - */ -exports._addToContainedEdges = function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - if (parentNode.containedEdges[childNode.id] === undefined) { - 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 - */ -exports._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); - } -}; - - -/** - * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain - * these edges inside of the cluster. - * - * @param parentNode - * @param childNode - * @private - */ -exports._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 - */ -exports._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 - */ -exports._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 - */ -exports._validateEdges = function(parentNode) { - var dynamicEdges = [] - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - if (parentNode.id == edge.toId || parentNode.id == edge.fromId) { - dynamicEdges.push(edge); - } - } - parentNode.dynamicEdges = dynamicEdges; -}; - - -/** - * 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 - */ -exports._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) - */ -exports.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(node.clusterSize + ":" + node.dynamicEdges.length); -// } -// } - -}; - - -/** - * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes - * if the rest of the nodes are already a few cluster levels in. - * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not - * clustered enough to the clusterToSmallestNeighbours function. - */ -exports.normalizeClusterLevels = function() { - var maxLevel = 0; - var minLevel = 1e9; - var clusterLevel = 0; - var nodeId; - - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - clusterLevel = this.nodes[nodeId].clusterSessions.length; - if (maxLevel < clusterLevel) {maxLevel = clusterLevel;} - if (minLevel > clusterLevel) {minLevel = clusterLevel;} - } - } - - if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { - var amountOfNodes = this.nodeIndices.length; - var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].clusterSessions.length < targetLevel) { - this._clusterToSmallestNeighbour(this.nodes[nodeId]); - } - } - } - this._updateNodeIndexList(); - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; - } - } -}; - - - -/** - * 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 - */ -exports._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. - * - */ -exports.repositionNodes = function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - if ((node.xFixed == false || node.yFixed == false)) { - var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.options.mass); - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} - this._repositionBezierNodes(node); - } - } -}; - - -/** - * 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 - */ -exports._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.dynamicEdges.length > largestHub) { - largestHub = node.dynamicEdges.length; - } - average += node.dynamicEdges.length; - averageSquared += Math.pow(node.dynamicEdges.length,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 - */ -exports._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].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 - */ -exports._getChainFraction = function() { - var chains = 0; - var total = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdges.length == 2) { - chains += 1; - } - total += 1; - } - } - return chains/total; -}; diff --git a/lib/network/mixins/HierarchicalLayoutMixin.js b/lib/network/mixins/HierarchicalLayoutMixin.js index 4d4cd54e..1fe143af 100644 --- a/lib/network/mixins/HierarchicalLayoutMixin.js +++ b/lib/network/mixins/HierarchicalLayoutMixin.js @@ -1,7 +1,7 @@ exports._resetLevels = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; if (node.preassignedLevel == false) { node.level = -1; node.hierarchyEnumerated = false; @@ -24,9 +24,9 @@ exports._setupHierarchicalLayout = function() { var definedLevel = false; var undefinedLevel = false; - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; if (node.level != -1) { definedLevel = true; } @@ -64,11 +64,8 @@ exports._setupHierarchicalLayout = function() { // check the distribution of the nodes per level. var distribution = this._getDistribution(); - // place the nodes on the canvas. This also stablilizes the system. + // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize. this._placeNodesByHierarchy(distribution); - - // start the simulation. - this.start(); } } }; @@ -129,9 +126,9 @@ exports._getDistribution = function() { // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. // the fix of X is removed after the x value has been set. - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; node.xFixed = true; node.yFixed = true; if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { @@ -181,9 +178,9 @@ exports._determineLevels = function(hubsize) { var nodeId, node; // determine hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; if (node.edges.length == hubsize) { node.level = 0; } @@ -191,9 +188,9 @@ exports._determineLevels = function(hubsize) { } // branch from hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; if (node.level == 0) { this._setLevel(1,node.edges,node.id); } @@ -214,22 +211,22 @@ exports._determineLevelsDirected = function() { var minLevel = 10000; // set first node to source - firstNode = this.nodes[this.nodeIndices[0]]; + firstNode = this.body.nodes[this.nodeIndices[0]]; firstNode.level = minLevel; this._setLevelDirected(minLevel,firstNode.edges,firstNode.id); // get the minimum level - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; minLevel = node.level < minLevel ? node.level : minLevel; } } // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; node.level -= minLevel; } } @@ -357,7 +354,7 @@ exports._setLevel = function(level, edges, parentId) { * @private */ exports._setLevelDirected = function(level, edges, parentId) { - this.nodes[parentId].hierarchyEnumerated = true; + this.body.nodes[parentId].hierarchyEnumerated = true; var childNode, direction; for (var i = 0; i < edges.length; i++) { direction = 1; @@ -390,10 +387,10 @@ exports._setLevelDirected = function(level, edges, parentId) { * @private */ exports._restoreNodes = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.nodes[nodeId].xFixed = false; - this.nodes[nodeId].yFixed = false; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].xFixed = false; + this.body.nodes[nodeId].yFixed = false; } } }; diff --git a/lib/network/mixins/ManipulationMixin.js b/lib/network/mixins/ManipulationMixin.js index 26f634d8..9052b780 100644 --- a/lib/network/mixins/ManipulationMixin.js +++ b/lib/network/mixins/ManipulationMixin.js @@ -15,10 +15,10 @@ exports._clearManipulatorBar = function() { this._cleanManipulatorHammers(); this._manipulationReleaseOverload = function () {}; - delete this.sectors['support']['nodes']['targetNode']; - delete this.sectors['support']['nodes']['targetViaNode']; + delete this.body.sectors['support']['nodes']['targetNode']; + delete this.body.sectors['support']['nodes']['targetViaNode']; this.controlNodesActive = false; - this.freezeSimulation(false); + this.freezeSimulationEnabled = false; }; @@ -276,7 +276,7 @@ exports._createAddEdgeToolbar = function() { // clear the toolbar this._clearManipulatorBar(); this._unselectAll(true); - this.freezeSimulation(true); + this.freezeSimulationEnabled = true; if (this.boundFunction) { this.off('select', this.boundFunction); @@ -284,7 +284,6 @@ exports._createAddEdgeToolbar = function() { var locale = this.constants.locales[this.constants.locale]; - this._unselectAll(); this.forceAppendSelection = false; this.blockConnectingEdgeSelection = true; @@ -466,22 +465,22 @@ exports._handleConnect = function(pointer) { var node = this._getNodeAt(pointer); if (node != null) { - if (node.clusterSize > 1) { + if (this.isCluster(node.id) == true) { alert(this.constants.locales[this.constants.locale]['createEdgeError']) } else { this._selectObject(node,false); - var supportNodes = this.sectors['support']['nodes']; + var supportNodes = this.body.sectors['support']['nodes']; // create a node the temporary line can look at - supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); + supportNodes['targetNode'] = this.body.functions.createNode({id:'targetNode'}); var targetNode = supportNodes['targetNode']; targetNode.x = node.x; targetNode.y = node.y; // create a temporary edge - this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants); - var connectionEdge = this.edges['connectionEdge']; + this.body.edges['connectionEdge'] = this.body.functions.createEdge({id:"connectionEdge",from:node.id,to:targetNode.id}); + var connectionEdge = this.body.edges['connectionEdge']; connectionEdge.from = node; connectionEdge.connected = true; connectionEdge.options.smoothCurves = {enabled: true, @@ -495,15 +494,12 @@ exports._handleConnect = function(pointer) { this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; var me = this; this._handleOnDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - var connectionEdge = me.edges['connectionEdge']; + var pointer = me._getPointer(event.gesture.center); + var connectionEdge = me.body.edges['connectionEdge']; connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x); connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y); me._redraw(); }; - - this.moving = true; - this.start(); } } } @@ -517,16 +513,16 @@ exports._finishConnect = function(event) { delete this.cachedFunctions["_handleOnDrag"]; // remember the edge id - var connectFromId = this.edges['connectionEdge'].fromId; + var connectFromId = this.body.edges['connectionEdge'].fromId; // remove the temporary nodes and edge - delete this.edges['connectionEdge']; - delete this.sectors['support']['nodes']['targetNode']; - delete this.sectors['support']['nodes']['targetViaNode']; + delete this.body.edges['connectionEdge']; + delete this.body.sectors['support']['nodes']['targetNode']; + delete this.body.sectors['support']['nodes']['targetViaNode']; var node = this._getNodeAt(pointer); if (node != null) { - if (node.clusterSize > 1) { + if (this.isCluster(node.id) === true) { alert(this.constants.locales[this.constants.locale]["createEdgeError"]) } else { @@ -550,7 +546,7 @@ exports._addNode = function() { if (this.triggerFunctions.add.length == 2) { var me = this; this.triggerFunctions.add(defaultData, function(finalizedData) { - me.nodesData.add(finalizedData); + me.body.data.nodes.add(finalizedData); me._createManipulatorBar(); me.moving = true; me.start(); @@ -564,7 +560,7 @@ exports._addNode = function() { } } else { - this.nodesData.add(defaultData); + this.body.data.nodes.add(defaultData); this._createManipulatorBar(); this.moving = true; this.start(); @@ -585,7 +581,7 @@ exports._createEdge = function(sourceNodeId,targetNodeId) { if (this.triggerFunctions.connect.length == 2) { var me = this; this.triggerFunctions.connect(defaultData, function(finalizedData) { - me.edgesData.add(finalizedData); + me.body.data.edges.add(finalizedData); me.moving = true; me.start(); }); @@ -597,7 +593,7 @@ exports._createEdge = function(sourceNodeId,targetNodeId) { } } else { - this.edgesData.add(defaultData); + this.body.data.edges.add(defaultData); this.moving = true; this.start(); } @@ -612,11 +608,12 @@ exports._createEdge = function(sourceNodeId,targetNodeId) { exports._editEdge = function(sourceNodeId,targetNodeId) { if (this.editMode == true) { var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; + console.log(defaultData); if (this.triggerFunctions.editEdge) { if (this.triggerFunctions.editEdge.length == 2) { var me = this; this.triggerFunctions.editEdge(defaultData, function(finalizedData) { - me.edgesData.update(finalizedData); + me.body.data.edges.update(finalizedData); me.moving = true; me.start(); }); @@ -628,7 +625,7 @@ exports._editEdge = function(sourceNodeId,targetNodeId) { } } else { - this.edgesData.update(defaultData); + this.body.data.edges.update(defaultData); this.moving = true; this.start(); } @@ -658,7 +655,7 @@ exports._editNode = function() { if (this.triggerFunctions.edit.length == 2) { var me = this; this.triggerFunctions.edit(data, function (finalizedData) { - me.nodesData.update(finalizedData); + me.body.data.nodes.update(finalizedData); me._createManipulatorBar(); me.moving = true; me.start(); @@ -691,8 +688,8 @@ exports._deleteSelected = function() { var data = {nodes: selectedNodes, edges: selectedEdges}; if (this.triggerFunctions.del.length == 2) { this.triggerFunctions.del(data, function (finalizedData) { - me.edgesData.remove(finalizedData.edges); - me.nodesData.remove(finalizedData.nodes); + me.body.data.edges.remove(finalizedData.edges); + me.body.data.nodes.remove(finalizedData.nodes); me._unselectAll(); me.moving = true; me.start(); @@ -703,8 +700,8 @@ exports._deleteSelected = function() { } } else { - this.edgesData.remove(selectedEdges); - this.nodesData.remove(selectedNodes); + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); this._unselectAll(); this.moving = true; this.start(); diff --git a/lib/network/mixins/MixinLoader.js b/lib/network/mixins/MixinLoader.js index cd7e96cf..568e15d6 100644 --- a/lib/network/mixins/MixinLoader.js +++ b/lib/network/mixins/MixinLoader.js @@ -1,6 +1,3 @@ -var PhysicsMixin = require('./physics/PhysicsMixin'); -var ClusterMixin = require('./ClusterMixin'); -var SectorsMixin = require('./SectorsMixin'); var SelectionMixin = require('./SelectionMixin'); var ManipulationMixin = require('./ManipulationMixin'); var NavigationMixin = require('./NavigationMixin'); @@ -53,43 +50,6 @@ exports._loadPhysicsSystem = function () { }; -/** - * Mixin the cluster system and initialize the parameters required. - * - * @private - */ -exports._loadClusterSystem = function () { - this.clusterSession = 0; - this.hubThreshold = 5; - this._loadMixin(ClusterMixin); -}; - - -/** - * Mixin the sector system and initialize the parameters required - * - * @private - */ -exports._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["support"] = {"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 - - this._loadMixin(SectorsMixin); -}; /** diff --git a/lib/network/mixins/SectorsMixin.js b/lib/network/mixins/SectorsMixin.js index 22b53965..802b4422 100644 --- a/lib/network/mixins/SectorsMixin.js +++ b/lib/network/mixins/SectorsMixin.js @@ -16,9 +16,9 @@ var Node = require('../Node'); * @private */ exports._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.body.sectors["active"][this._sector()].nodes = this.body.nodes; + this.body.sectors["active"][this._sector()].edges = this.body.edges; + this.body.sectors["active"][this._sector()].nodeIndices = this.body.nodeIndices; }; @@ -49,9 +49,9 @@ exports._switchToSector = function(sectorId, sectorType) { * @private */ exports._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.body.nodeIndices = this.body.sectors["active"][sectorId]["nodeIndices"]; + this.body.nodes = this.body.sectors["active"][sectorId]["nodes"]; + this.body.edges = this.body.sectors["active"][sectorId]["edges"]; }; @@ -62,9 +62,9 @@ exports._switchToActiveSector = function(sectorId) { * @private */ exports._switchToSupportSector = function() { - this.nodeIndices = this.sectors["support"]["nodeIndices"]; - this.nodes = this.sectors["support"]["nodes"]; - this.edges = this.sectors["support"]["edges"]; + this.body.nodeIndices = this.body.sectors["support"]["nodeIndices"]; + this.body.nodes = this.body.sectors["support"]["nodes"]; + this.body.edges = this.body.sectors["support"]["edges"]; }; @@ -76,9 +76,9 @@ exports._switchToSupportSector = function() { * @private */ exports._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.body.nodeIndices = this.body.sectors["frozen"][sectorId]["nodeIndices"]; + this.body.nodes = this.body.sectors["frozen"][sectorId]["nodes"]; + this.body.edges = this.body.sectors["frozen"][sectorId]["edges"]; }; @@ -153,21 +153,21 @@ exports._forgetLastSector = function() { */ exports._createNewSector = function(newId) { // create the new sector - this.sectors["active"][newId] = {"nodes":{}, + this.body.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( + this.body.sectors["active"][newId]['drawingNode'] = new Node( {id:newId, color: { background: "#eaefef", border: "495c5e" } },{},{},this.constants); - this.sectors["active"][newId]['drawingNode'].clusterSize = 2; + this.body.sectors["active"][newId]['drawingNode'].clusterSize = 2; }; @@ -179,7 +179,7 @@ exports._createNewSector = function(newId) { * @private */ exports._deleteActiveSector = function(sectorId) { - delete this.sectors["active"][sectorId]; + delete this.body.sectors["active"][sectorId]; }; @@ -191,7 +191,7 @@ exports._deleteActiveSector = function(sectorId) { * @private */ exports._deleteFrozenSector = function(sectorId) { - delete this.sectors["frozen"][sectorId]; + delete this.body.sectors["frozen"][sectorId]; }; @@ -204,7 +204,7 @@ exports._deleteFrozenSector = function(sectorId) { */ exports._freezeSector = function(sectorId) { // we move the set references from the active to the frozen stack. - this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; + this.body.sectors["frozen"][sectorId] = this.body.sectors["active"][sectorId]; // we have moved the sector data into the frozen set, we now remove it from the active set this._deleteActiveSector(sectorId); @@ -220,7 +220,7 @@ exports._freezeSector = function(sectorId) { */ exports._activateSector = function(sectorId) { // we move the set references from the frozen to the active stack. - this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; + this.body.sectors["active"][sectorId] = this.body.sectors["frozen"][sectorId]; // we have moved the sector data into the active set, we now remove it from the frozen stack this._deleteFrozenSector(sectorId); @@ -238,22 +238,22 @@ exports._activateSector = function(sectorId) { */ exports._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]; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.sectors["frozen"][sectorId]["nodes"][nodeId] = this.body.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]; + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.sectors["frozen"][sectorId]["edges"][edgeId] = this.body.edges[edgeId]; } } // merge the nodeIndices - for (var i = 0; i < this.nodeIndices.length; i++) { - this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); + for (var i = 0; i < this.body.nodeIndices.length; i++) { + this.body.sectors["frozen"][sectorId]["nodeIndices"].push(this.body.nodeIndices[i]); } }; @@ -280,7 +280,7 @@ exports._addSector = function(node) { var sector = this._sector(); // // this should allow me to select nodes from a frozen set. -// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { +// if (this.body.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { // console.log("the node is part of the active sector"); // } // else { @@ -288,7 +288,7 @@ exports._addSector = function(node) { // } // 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]; + delete this.body.nodes[node.id]; var unqiueIdentifier = util.randomUUID(); @@ -305,7 +305,7 @@ exports._addSector = function(node) { 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; + this.body.nodes[node.id] = node; }; @@ -321,9 +321,9 @@ exports._collapseSector = function() { // 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)) { + if ((this.body.nodeIndices.length == 1) || + (this.body.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (this.body.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 @@ -368,8 +368,8 @@ exports._collapseSector = function() { exports._doInAllActiveSectors = function(runFunction,argument) { var returnValues = []; if (argument === undefined) { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { + for (var sector in this.body.sectors["active"]) { + if (this.body.sectors["active"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToActiveSector(sector); returnValues.push( this[runFunction]() ); @@ -377,8 +377,8 @@ exports._doInAllActiveSectors = function(runFunction,argument) { } } else { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { + for (var sector in this.body.sectors["active"]) { + if (this.body.sectors["active"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToActiveSector(sector); var args = Array.prototype.splice.call(arguments, 1); @@ -439,8 +439,8 @@ exports._doInSupportSector = function(runFunction,argument) { */ exports._doInAllFrozenSectors = function(runFunction,argument) { if (argument === undefined) { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { + for (var sector in this.body.sectors["frozen"]) { + if (this.body.sectors["frozen"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToFrozenSector(sector); this[runFunction](); @@ -448,8 +448,8 @@ exports._doInAllFrozenSectors = function(runFunction,argument) { } } else { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { + for (var sector in this.body.sectors["frozen"]) { + if (this.body.sectors["frozen"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToFrozenSector(sector); var args = Array.prototype.splice.call(arguments, 1); @@ -495,15 +495,15 @@ exports._doInAllSectors = function(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. + * This clears the nodeIndices list. We cannot use this.body.nodeIndices = [] because we would break the link with the + * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.body.nodeIndices to it. * * @private */ exports._clearNodeIndexList = function() { var sector = this._sector(); - this.sectors["active"][sector]["nodeIndices"] = []; - this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; + this.body.sectors["active"][sector]["nodeIndices"] = []; + this.body.nodeIndices = this.body.sectors["active"][sector]["nodeIndices"]; }; @@ -516,16 +516,16 @@ exports._clearNodeIndexList = function() { */ exports._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) { + for (var sector in this.body.sectors[sectorType]) { + if (this.body.sectors[sectorType].hasOwnProperty(sector)) { + if (this.body.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]; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.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;} @@ -533,7 +533,7 @@ exports._drawSectorNodes = function(ctx,sectorType) { if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} } } - node = this.sectors[sectorType][sector]["drawingNode"]; + node = this.body.sectors[sectorType][sector]["drawingNode"]; node.x = 0.5 * (maxX + minX); node.y = 0.5 * (maxY + minY); node.width = 2 * (node.x - minX); diff --git a/lib/network/mixins/SelectionMixin.js b/lib/network/mixins/SelectionMixin.js index 51c1e700..09d74c91 100644 --- a/lib/network/mixins/SelectionMixin.js +++ b/lib/network/mixins/SelectionMixin.js @@ -1,14 +1,13 @@ var Node = require('../Node'); /** - * This function can be called from the _doInAllSectors function * * @param object * @param overlappingNodes * @private */ exports._getNodesOverlappingWith = function(object, overlappingNodes) { - var nodes = this.nodes; + var nodes = this.body.nodes; for (var nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { if (nodes[nodeId].isOverlappingWith(object)) { @@ -26,7 +25,7 @@ exports._getNodesOverlappingWith = function(object, overlappingNodes) { */ exports._getAllNodesOverlappingWith = function (object) { var overlappingNodes = []; - this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); + this._getNodesOverlappingWith(object,overlappingNodes); return overlappingNodes; }; @@ -66,7 +65,7 @@ exports._getNodeAt = function (pointer) { // 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]]; + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; } else { return null; @@ -81,7 +80,7 @@ exports._getNodeAt = function (pointer) { * @private */ exports._getEdgesOverlappingWith = function (object, overlappingEdges) { - var edges = this.edges; + var edges = this.body.edges; for (var edgeId in edges) { if (edges.hasOwnProperty(edgeId)) { if (edges[edgeId].isOverlappingWith(object)) { @@ -100,7 +99,7 @@ exports._getEdgesOverlappingWith = function (object, overlappingEdges) { */ exports._getAllEdgesOverlappingWith = function (object) { var overlappingEdges = []; - this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); + this._getEdgesOverlappingWith(object,overlappingEdges); return overlappingEdges; }; @@ -117,7 +116,7 @@ exports._getEdgeAt = function(pointer) { var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); if (overlappingEdges.length > 0) { - return this.edges[overlappingEdges[overlappingEdges.length - 1]]; + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; } else { return null; @@ -355,8 +354,8 @@ exports._clusterInSelection = function() { * @private */ exports._selectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; edge.select(); this._addToSelection(edge); } @@ -369,8 +368,8 @@ exports._selectConnectedEdges = function(node) { * @private */ exports._hoverConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; edge.hover = true; this._addToHover(edge); } @@ -384,8 +383,8 @@ exports._hoverConnectedEdges = function(node) { * @private */ exports._unselectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; edge.unselect(); this._removeFromSelection(edge); } @@ -650,7 +649,7 @@ exports.selectNodes = function(selection, highlightEdges) { for (i = 0, iMax = selection.length; i < iMax; i++) { id = selection[i]; - var node = this.nodes[id]; + var node = this.body.nodes[id]; if (!node) { throw new RangeError('Node with id "' + id + '" not found'); } @@ -677,7 +676,7 @@ exports.selectEdges = function(selection) { for (i = 0, iMax = selection.length; i < iMax; i++) { id = selection[i]; - var edge = this.edges[id]; + var edge = this.body.edges[id]; if (!edge) { throw new RangeError('Edge with id "' + id + '" not found'); } @@ -693,14 +692,14 @@ exports.selectEdges = function(selection) { exports._updateSelection = function () { for(var nodeId in this.selectionObj.nodes) { if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { delete this.selectionObj.nodes[nodeId]; } } } for(var edgeId in this.selectionObj.edges) { if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { delete this.selectionObj.edges[edgeId]; } } diff --git a/lib/network/mixins/physics/BarnesHutMixin.js b/lib/network/mixins/physics/BarnesHutMixin.js deleted file mode 100644 index b70f474c..00000000 --- a/lib/network/mixins/physics/BarnesHutMixin.js +++ /dev/null @@ -1,399 +0,0 @@ -/** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private - */ -exports._calculateNodeForces = function() { - if (this.constants.physics.barnesHut.gravitationalConstant != 0) { - var node; - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - var nodeCount = nodeIndices.length; - - this._formBarnesHutTree(nodes,nodeIndices); - - var barnesHutTree = this.barnesHutTree; - - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHut condition - this._getForceContribution(barnesHutTree.root.children.NW,node); - this._getForceContribution(barnesHutTree.root.children.NE,node); - this._getForceContribution(barnesHutTree.root.children.SW,node); - this._getForceContribution(barnesHutTree.root.children.SE,node); - } - } - } -}; - - -/** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ -exports._getForceContribution = function(parentBranch,node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx,dy,distance; - - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // BarnesHut condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; - } - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } - else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW,node); - this._getForceContribution(parentBranch.children.NE,node); - this._getForceContribution(parentBranch.children.SW,node); - this._getForceContribution(parentBranch.children.SE,node); - } - else { // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5*Math.random(); - dx = distance; - } - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } - } - } - } -}; - -/** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices - * @private - */ -exports._formBarnesHutTree = function(nodes,nodeIndices) { - var node; - var nodeCount = nodeIndices.length; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX =-Number.MAX_VALUE, - maxY =-Number.MAX_VALUE; - - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { minX = x; } - if (x > maxX) { maxX = x; } - if (y < minY) { minY = y; } - if (y > maxY) { maxY = y; } - } - } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize - else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize - - - var minimumTreeSize = 1e-5; - var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); - - // construct the barnesHutTree - var barnesHutTree = { - root:{ - centerOfMass: {x:0, y:0}, - mass:0, - range: { - minX: centerX-halfRootSize,maxX:centerX+halfRootSize, - minY: centerY-halfRootSize,maxY:centerY+halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data:null}, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); - - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root,node); - } - } - - // make global - this.barnesHutTree = barnesHutTree -}; - - -/** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private - */ -exports._updateBranchMass = function(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1/totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; - - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); - parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; - -}; - - -/** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate - * @private - */ -exports._placeInTree = function(parentBranch,node,skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch,node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { // in NW - this._placeInRegion(parentBranch,node,"NW"); - } - else { // in SW - this._placeInRegion(parentBranch,node,"SW"); - } - } - else { // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { // in NE - this._placeInRegion(parentBranch,node,"NE"); - } - else { // in SE - this._placeInRegion(parentBranch,node,"SE"); - } - } -}; - - -/** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region - * @private - */ -exports._placeInRegion = function(parentBranch,node,region) { - switch (parentBranch.children[region].childrenCount) { - case 0: // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region],node); - break; - case 1: // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && - parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } - else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region],node); - } - break; - case 4: // place in branch - this._placeInTree(parentBranch.children[region],node); - break; - } -}; - - -/** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch - * @private - */ -exports._splitBranch = function(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch,"NW"); - this._insertRegion(parentBranch,"NE"); - this._insertRegion(parentBranch,"SW"); - this._insertRegion(parentBranch,"SE"); - - if (containedNode != null) { - this._placeInTree(parentBranch,containedNode); - } -}; - - -/** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange - * @private - */ -exports._insertRegion = function(parentBranch, region) { - var minX,maxX,minY,maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - } - - - parentBranch.children[region] = { - centerOfMass:{x:0,y:0}, - mass:0, - range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: {data:null}, - maxWidth: 0, - level: parentBranch.level+1, - childrenCount: 0 - }; -}; - - -/** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color - * @private - */ -exports._drawTree = function(ctx,color) { - if (this.barnesHutTree !== undefined) { - - ctx.lineWidth = 1; - - this._drawBranch(this.barnesHutTree.root,ctx,color); - } -}; - - -/** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color - * @private - */ -exports._drawBranch = function(branch,ctx,color) { - if (color === undefined) { - color = "#FF0000"; - } - - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW,ctx); - this._drawBranch(branch.children.NE,ctx); - this._drawBranch(branch.children.SE,ctx); - this._drawBranch(branch.children.SW,ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.minY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.minY); - ctx.stroke(); - - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ -}; diff --git a/lib/network/mixins/physics/HierarchialRepulsionMixin.js b/lib/network/mixins/physics/HierarchialRepulsionMixin.js deleted file mode 100644 index 774b4257..00000000 --- a/lib/network/mixins/physics/HierarchialRepulsionMixin.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Calculate the forces the nodes apply on eachother based on a repulsion field. - * This field is linearly approximated. - * - * @private - */ -exports._calculateNodeForces = function () { - var dx, dy, distance, fx, fy, - repulsingForce, node1, node2, i, j; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - // repulsing forces between nodes - var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - - // nodes only affect nodes on their level - if (node1.level == node2.level) { - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); - } - else { - repulsingForce = 0; - } - // normalize force with - if (distance == 0) { - distance = 0.01; - } - else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; - - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; - } - } - } -}; - - -/** - * this function calculates the effects of the springs in the case of unsmooth curves. - * - * @private - */ -exports._calculateHierarchicalSpringForces = function () { - var edgeLength, edge, edgeId; - var dx, dy, fx, fy, springForce, distance; - var edges = this.edges; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - - for (var i = 0; i < nodeIndices.length; i++) { - var node1 = nodes[nodeIndices[i]]; - node1.springFx = 0; - node1.springFy = 0; - } - - - // 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)) { - edgeLength = edge.physics.springLength; - // this implies that the edges between big clusters are longer - edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; - - dx = (edge.from.x - edge.to.x); - dy = (edge.from.y - edge.to.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - - - if (edge.to.level != edge.from.level) { - edge.to.springFx -= fx; - edge.to.springFy -= fy; - edge.from.springFx += fx; - edge.from.springFy += fy; - } - else { - var factor = 0.5; - edge.to.fx -= factor*fx; - edge.to.fy -= factor*fy; - edge.from.fx += factor*fx; - edge.from.fy += factor*fy; - } - } - } - } - } - - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); - springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); - - node.fx += springFx; - node.fy += springFy; - } - - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - totalFx += node.fx; - totalFy += node.fy; - } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; - - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - node.fx -= correctionFx; - node.fy -= correctionFy; - } - -}; \ No newline at end of file diff --git a/lib/network/mixins/physics/PhysicsMixin.js b/lib/network/mixins/physics/PhysicsMixin.js deleted file mode 100644 index c17cacf7..00000000 --- a/lib/network/mixins/physics/PhysicsMixin.js +++ /dev/null @@ -1,724 +0,0 @@ -var util = require('../../../util'); -var RepulsionMixin = require('./RepulsionMixin'); -var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin'); -var BarnesHutMixin = require('./BarnesHutMixin'); - -/** - * Toggling barnes Hut calculation on and off. - * - * @private - */ -exports._toggleBarnesHut = function () { - this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; - this._loadSelectedForceSolver(); - this.moving = true; - this.start(); -}; - - -/** - * This loads the node force solver based on the barnes hut or repulsion algorithm - * - * @private - */ -exports._loadSelectedForceSolver = function () { - // this overloads the this._calculateNodeForces - if (this.constants.physics.barnesHut.enabled == true) { - this._clearMixin(RepulsionMixin); - this._clearMixin(HierarchialRepulsionMixin); - - this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; - this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; - this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; - this.constants.physics.damping = this.constants.physics.barnesHut.damping; - - this._loadMixin(BarnesHutMixin); - } - else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._clearMixin(BarnesHutMixin); - this._clearMixin(RepulsionMixin); - - this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; - this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; - this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; - this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; - - this._loadMixin(HierarchialRepulsionMixin); - } - else { - this._clearMixin(BarnesHutMixin); - this._clearMixin(HierarchialRepulsionMixin); - this.barnesHutTree = undefined; - - this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; - this.constants.physics.springLength = this.constants.physics.repulsion.springLength; - this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; - this.constants.physics.damping = this.constants.physics.repulsion.damping; - - this._loadMixin(RepulsionMixin); - } -}; - -/** - * 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 - */ -exports._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 - */ -exports._calculateForces = function () { - // 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 - - this._calculateGravitationalForces(); - this._calculateNodeForces(); - - if (this.constants.physics.springConstant > 0) { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._calculateSpringForcesWithSupport(); - } - else { - if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._calculateHierarchicalSpringForces(); - } - else { - this._calculateSpringForces(); - } - } - } -}; - - -/** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.nodes with the support nodes. - * - * @private - */ -exports._updateCalculationNodes = function () { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this.calculationNodes = {}; - this.calculationNodeIndices = []; - - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId] = this.nodes[nodeId]; - } - } - var supportNodes = this.sectors['support']['nodes']; - for (var supportNodeId in supportNodes) { - if (supportNodes.hasOwnProperty(supportNodeId)) { - if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { - this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; - } - else { - supportNodes[supportNodeId]._setForce(0, 0); - } - } - } - - for (var idx in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(idx)) { - this.calculationNodeIndices.push(idx); - } - } - } - else { - this.calculationNodes = this.nodes; - this.calculationNodeIndices = this.nodeIndices; - } -}; - - -/** - * this function applies the central gravity effect to keep groups from floating off - * - * @private - */ -exports._calculateGravitationalForces = function () { - var dx, dy, distance, node, i; - var nodes = this.calculationNodes; - var gravity = this.constants.physics.centralGravity; - var gravityForce = 0; - - for (i = 0; i < this.calculationNodeIndices.length; i++) { - node = nodes[this.calculationNodeIndices[i]]; - node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default" && gravity != 0) { - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - gravityForce = (distance == 0) ? 0 : (gravity / distance); - node.fx = dx * gravityForce; - node.fy = dy * gravityForce; - } - else { - node.fx = 0; - node.fy = 0; - } - } -}; - - - - -/** - * this function calculates the effects of the springs in the case of unsmooth curves. - * - * @private - */ -exports._calculateSpringForces = function () { - var edgeLength, edge, edgeId; - var dx, dy, fx, fy, springForce, distance; - var edges = this.edges; - - // 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)) { - edgeLength = edge.physics.springLength; - // this implies that the edges between big clusters are longer - edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; - - dx = (edge.from.x - edge.to.x); - dy = (edge.from.y - edge.to.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - edge.from.fx += fx; - edge.from.fy += fy; - edge.to.fx -= fx; - edge.to.fy -= fy; - } - } - } - } -}; - - - - -/** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * - * @private - */ -exports._calculateSpringForcesWithSupport = function () { - var edgeLength, edge, edgeId, combinedClusterSize; - var edges = this.edges; - - // 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)) { - if (edge.via != null) { - var node1 = edge.to; - var node2 = edge.via; - var node3 = edge.from; - - edgeLength = edge.physics.springLength; - - combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; - - // this implies that the edges between big clusters are longer - edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } - } - } - } - } -}; - - -/** - * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. - * - * @param node1 - * @param node2 - * @param edgeLength - * @private - */ -exports._calculateSpringForce = function (node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = (node1.x - node2.x); - dy = (node1.y - node2.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - node1.fx += fx; - node1.fy += fy; - node2.fx -= fx; - node2.fy -= fy; -}; - - -exports._cleanupPhysicsConfiguration = function() { - if (this.physicsConfiguration !== undefined) { - while (this.physicsConfiguration.hasChildNodes()) { - this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild); - } - - this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration); - this.physicsConfiguration = undefined; - } -} - -/** - * Load the HTML for the physics config and bind it - * @private - */ -exports._loadPhysicsConfiguration = function () { - if (this.physicsConfiguration === undefined) { - this.backupConstants = {}; - util.deepExtend(this.backupConstants,this.constants); - - var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10); - var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10) - - var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; - this.physicsConfiguration = document.createElement('div'); - this.physicsConfiguration.className = "PhysicsConfiguration"; - this.physicsConfiguration.innerHTML = '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Options:
' - this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); - this.optionsDiv = document.createElement("div"); - this.optionsDiv.style.fontSize = "14px"; - this.optionsDiv.style.fontFamily = "verdana"; - this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); - - var rangeElement; - rangeElement = document.getElementById('graph_BH_gc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); - rangeElement = document.getElementById('graph_BH_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_BH_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_BH_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_BH_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_R_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); - rangeElement = document.getElementById('graph_R_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_R_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_R_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_R_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_H_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - rangeElement = document.getElementById('graph_H_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_H_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_H_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_H_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); - rangeElement = document.getElementById('graph_H_direction'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); - rangeElement = document.getElementById('graph_H_levsep'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); - rangeElement = document.getElementById('graph_H_nspac'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); - - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - var radioButton3 = document.getElementById("graph_physicsMethod3"); - radioButton2.checked = true; - if (this.constants.physics.barnesHut.enabled) { - radioButton1.checked = true; - } - if (this.constants.hierarchicalLayout.enabled) { - radioButton3.checked = true; - } - - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - var graph_repositionNodes = document.getElementById("graph_repositionNodes"); - var graph_generateOptions = document.getElementById("graph_generateOptions"); - - graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); - graph_repositionNodes.onclick = graphRepositionNodes.bind(this); - graph_generateOptions.onclick = graphGenerateOptions.bind(this); - if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { - graph_toggleSmooth.style.background = "#A4FF56"; - } - else { - graph_toggleSmooth.style.background = "#FF8532"; - } - - - switchConfigurations.apply(this); - - radioButton1.onchange = switchConfigurations.bind(this); - radioButton2.onchange = switchConfigurations.bind(this); - radioButton3.onchange = switchConfigurations.bind(this); - } -}; - -/** - * This overwrites the this.constants. - * - * @param constantsVariableName - * @param value - * @private - */ -exports._overWriteGraphConstants = function (constantsVariableName, value) { - var nameArray = constantsVariableName.split("_"); - if (nameArray.length == 1) { - this.constants[nameArray[0]] = value; - } - else if (nameArray.length == 2) { - this.constants[nameArray[0]][nameArray[1]] = value; - } - else if (nameArray.length == 3) { - this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; - } -}; - - -/** - * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. - */ -function graphToggleSmoothCurves () { - this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} - - this._configureSmoothCurves(false); -} - -/** - * this function is used to scramble the nodes - * - */ -function graphRepositionNodes () { - for (var nodeId in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; - this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; - } - } - if (this.constants.hierarchicalLayout.enabled == true) { - this._setupHierarchicalLayout(); - showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); - showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); - showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); - showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); - } - else { - this.repositionNodes(); - } - this.moving = true; - this.start(); -} - -/** - * this is used to generate an options file from the playing with physics system. - */ -function graphGenerateOptions () { - var options = "No options are required, default values used."; - var optionsSpecific = []; - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - if (radioButton1.checked == true) { - if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options = "var options = {"; - options += "physics: {barnesHut: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' - } - if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { - if (optionsSpecific.length == 0) {options = "var options = {";} - else {options += ", "} - options += "smoothCurves: " + this.constants.smoothCurves.enabled; - } - if (options != "No options are required, default values used.") { - options += '};' - } - } - else if (radioButton2.checked == true) { - options = "var options = {"; - options += "physics: {barnesHut: {enabled: false}"; - if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += ", repulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' - } - if (optionsSpecific.length == 0) {options += "}"} - if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { - options += ", smoothCurves: " + this.constants.smoothCurves; - } - options += '};' - } - else { - options = "var options = {"; - if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += "physics: {hierarchicalRepulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", "; - } - } - options += '}},'; - } - options += 'hierarchicalLayout: {'; - optionsSpecific = []; - if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} - if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} - if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} - if (optionsSpecific.length != 0) { - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}' - } - else { - options += "enabled:true}"; - } - options += '};' - } - - - this.optionsDiv.innerHTML = options; -} - -/** - * this is used to switch between barnesHut, repulsion and hierarchical. - * - */ -function switchConfigurations () { - var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; - var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; - var tableId = "graph_" + radioButton + "_table"; - var table = document.getElementById(tableId); - table.style.display = "block"; - for (var i = 0; i < ids.length; i++) { - if (ids[i] != tableId) { - table = document.getElementById(ids[i]); - table.style.display = "none"; - } - } - this._restoreNodes(); - if (radioButton == "R") { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = false; - } - else if (radioButton == "H") { - if (this.constants.hierarchicalLayout.enabled == false) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - this.constants.smoothCurves.enabled = false; - this._setupHierarchicalLayout(); - } - } - else { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = true; - } - this._loadSelectedForceSolver(); - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} - this.moving = true; - this.start(); -} - - -/** - * this generates the ranges depending on the iniital values. - * - * @param id - * @param map - * @param constantsVariableName - */ -function showValueOfRange (id,map,constantsVariableName) { - var valueId = id + "_value"; - var rangeValue = document.getElementById(id).value; - - if (Array.isArray(map)) { - document.getElementById(valueId).value = map[parseInt(rangeValue)]; - this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); - } - else { - document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); - this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); - } - - if (constantsVariableName == "hierarchicalLayout_direction" || - constantsVariableName == "hierarchicalLayout_levelSeparation" || - constantsVariableName == "hierarchicalLayout_nodeSpacing") { - this._setupHierarchicalLayout(); - } - this.moving = true; - this.start(); -} - - diff --git a/lib/network/mixins/physics/RepulsionMixin.js b/lib/network/mixins/physics/RepulsionMixin.js deleted file mode 100644 index 4b3ef6ac..00000000 --- a/lib/network/mixins/physics/RepulsionMixin.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * - * @private - */ -exports._calculateNodeForces = function () { - var dx, dy, angle, distance, fx, fy, combinedClusterSize, - repulsingForce, node1, node2, i, j; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - // approximation constants - var a_base = -2 / 3; - var b = 4 / 3; - - // repulsing forces between nodes - var nodeDistance = this.constants.physics.repulsion.nodeDistance; - var minimumDistance = nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // same condition as BarnesHut, making sure nodes are never 100% overlapping. - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; - } - - minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); - var a = a_base / minimumDistance; - if (distance < 2 * minimumDistance) { - if (distance < 0.5 * minimumDistance) { - repulsingForce = 1.0; - } - else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) - } - - // amplify the repulsion for clusters. - repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; - repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance); - - fx = dx * repulsingForce; - fy = dy * repulsingForce; - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; - - } - } - } -}; diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js new file mode 100644 index 00000000..73cd8b27 --- /dev/null +++ b/lib/network/modules/Canvas.js @@ -0,0 +1,229 @@ +/** + * Created by Alex on 26-Feb-15. + */ + + +var Hammer = require('../../module/hammer'); + +class Canvas { + /** + * Create the main frame for the Network. + * This function is executed once when a Network object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + * @private + */ + constructor(body, options) { + this.body = body; + this.setOptions(options); + + this.translation = {x: 0, y: 0}; + this.scale = 1.0; + this.body.emitter.on("_setScale", (scale) => {this.scale = scale}); + this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); + this.body.emitter.once("resize", (obj) => {this.translation.x = obj.width * 0.5; this.translation.y = obj.height * 0.5; this.body.emitter.emit("_setTranslation", this.translation)}); + + this.pixelRatio = 1; + + // remove all elements from the container element. + while (this.body.container.hasChildNodes()) { + this.body.container.removeChild(this.body.container.firstChild); + } + + this.frame = document.createElement('div'); + this.frame.className = 'vis network-frame'; + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + this.frame.tabIndex = 900; + + ////////////////////////////////////////////////////////////////// + + this.frame.canvas = document.createElement("canvas"); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement( 'DIV' ); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold' ; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } + else { + var ctx = this.frame.canvas.getContext("2d"); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1); + + //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + } + + // add the frame to the container element + this.body.container.appendChild(this.frame); + + this.body.emitter.emit("_setScale", 1);; + this.body.emitter.emit("_setTranslation", {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight});; + + this._bindHammer(); + } + + + /** + * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * @private + */ + _bindHammer() { + var me = this; + if (this.hammer !== undefined) { + this.hammer.dispose(); + } + this.drag = {}; + this.pinch = {}; + this.hammer = Hammer(this.frame.canvas, { + prevent_default: true + }); + this.hammer.on('tap', me.body.eventListeners.onTap ); + this.hammer.on('doubletap', me.body.eventListeners.onDoubleTap ); + this.hammer.on('hold', me.body.eventListeners.onHold ); + this.hammer.on('touch', me.body.eventListeners.onTouch ); + this.hammer.on('dragstart', me.body.eventListeners.onDragStart ); + this.hammer.on('drag', me.body.eventListeners.onDrag ); + this.hammer.on('dragend', me.body.eventListeners.onDragEnd ); + + if (this.options.zoomable == true) { + this.hammer.on('mousewheel', me.body.eventListeners.onMouseWheel.bind(me)); + this.hammer.on('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); // for FF + this.hammer.on('pinch', me.body.eventListeners.onPinch.bind(me) ); + } + + this.hammer.on('mousemove', me.body.eventListeners.onMouseMove.bind(me) ); + + this.hammerFrame = Hammer(this.frame, { + prevent_default: true + }); + this.hammerFrame.on('release', me.body.eventListeners.onRelease.bind(me) ); + } + + + setOptions(options = {}) { + this.options = options; + } + + /** + * Set a new size for the network + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') + */ + setSize(width, height) { + var emitEvent = false; + var oldWidth = this.frame.canvas.width; + var oldHeight = this.frame.canvas.height; + if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; + + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + + this.options.width = width; + this.options.height = height; + + emitEvent = true; + } + else { + // this would adapt the width of the canvas to the width from 100% if and only if + // there is a change. + + if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + emitEvent = true; + } + if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + emitEvent = true; + } + } + + if (emitEvent === true) { + this.body.emitter.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio}); + } + }; + + + /** + * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} x + * @returns {number} + * @private + */ + _XconvertDOMtoCanvas(x) { + return (x - this.translation.x) / this.scale; + } + + /** + * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the X coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} x + * @returns {number} + * @private + */ + _XconvertCanvasToDOM(x) { + return x * this.scale + this.translation.x; + } + + /** + * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} y + * @returns {number} + * @private + */ + _YconvertDOMtoCanvas(y) { + return (y - this.translation.y) / this.scale; + } + + /** + * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} y + * @returns {number} + * @private + */ + _YconvertCanvasToDOM(y) { + return y * this.scale + this.translation.y ; + } + + + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + canvasToDOM (pos) { + return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)}; + } + + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + DOMtoCanvas (pos) { + return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)}; + } + +} + +export {Canvas}; \ No newline at end of file diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js new file mode 100644 index 00000000..50bbb4df --- /dev/null +++ b/lib/network/modules/CanvasRenderer.js @@ -0,0 +1,246 @@ +/** + * Created by Alex on 26-Feb-15. + */ + +if (typeof window !== 'undefined') { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; +} + +class CanvasRenderer { + constructor(body) { + this.body = body; + + this.redrawRequested = false; + this.renderTimer = false; + this.requiresTimeout = true; + this.continueRendering = true; + this.renderRequests = 0; + + this.translation = {x: 0, y: 0}; + this.scale = 1.0; + this.canvasTopLeft = {x: 0, y: 0}; + this.canvasBottomRight = {x: 0, y: 0}; + + this.body.emitter.on("_setScale", (scale) => this.scale = scale); + this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); + this.body.emitter.on("_redraw", this._redraw.bind(this)); + this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true)); + this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); + this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.continueRendering = true; this.startRendering();}); + this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.continueRendering = this.renderRequests > 0;}); + + this._determineBrowserMethod(); + } + + + startRendering() { + if (this.continueRendering === true) { + if (!this.renderTimer) { + if (this.requiresTimeout == true) { + this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function + } + else { + this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function + } + } + } + else { + + } + } + + renderStep() { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; + + if (this.requiresTimeout == true) { + // this schedules a new simulation step + this.startRendering(); + } + + this._redraw(); + + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); + } + } + + setCanvas(canvas) { + this.canvas = canvas; + } + /** + * Redraw the network with the current data + * chart will be resized too. + */ + redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); + } + + /** + * Redraw the network with the current data + * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. + * @private + */ + _requestRedraw(hidden) { + if (this.redrawRequested !== true) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, hidden),0); + } + else { + window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); + } + } + } + + _redraw(hidden = false) { + this.body.emitter.emit("_beforeRender"); + + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext('2d'); + + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this.canvasTopLeft = this.canvas.DOMtoCanvas({x:0,y:0}); + this.canvasBottomRight = this.canvas.DOMtoCanvas({x:this.canvas.frame.canvas.clientWidth,y:this.canvas.frame.canvas.clientHeight}); + + if (hidden === false) { + // todo: solve this + //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { + this._drawEdges(ctx); + //} + } + + // todo: solve this + //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { + this._drawNodes(ctx, this.body.nodes, hidden); + //} + + if (hidden === false) { + if (this.controlNodesActive == true) { + this._drawControlNodes(ctx); + } + } + + //this._drawNodes(ctx,this.body.supportNodes,true); + // this.physics.nodesSolver._debug(ctx,"#F00F0F"); + + // restore original scaling and translation + ctx.restore(); + + if (hidden === true) { + ctx.clearRect(0, 0, w, h); + } + } + + + /** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] + * @private + */ + _drawNodes(ctx,nodes,alwaysShow = false) { + // first draw the unselected 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 (alwaysShow === true) { + nodes[id].draw(ctx); + } + else if (nodes[id].inArea() === true) { + nodes[id].draw(ctx); + } + } + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + if (nodes[selected[s]].inArea() || alwaysShow) { + nodes[selected[s]].draw(ctx); + } + } + } + + + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + _drawEdges(ctx) { + var edges = this.body.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.setScale(this.scale); + if (edge.connected === true) { + edges[id].draw(ctx); + } + } + } + } + + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + _drawControlNodes(ctx) { + var edges = this.body.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + edges[id]._drawControlNodes(ctx); + } + } + } + + /** + * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because + * some implementations (safari and IE9) did not support requestAnimationFrame + * @private + */ + _determineBrowserMethod() { + if (typeof window !== 'undefined') { + var browserType = navigator.userAgent.toLowerCase(); + this.requiresTimeout = false; + if (browserType.indexOf('msie 9.0') != -1) { // IE 9 + this.requiresTimeout = true; + } + else if (browserType.indexOf('safari') != -1) { // safari + if (browserType.indexOf('chrome') <= -1) { + this.requiresTimeout = true; + } + } + } + else { + this.requiresTimeout = true; + } + } + +} + +export {CanvasRenderer}; \ No newline at end of file diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js new file mode 100644 index 00000000..186c9fc3 --- /dev/null +++ b/lib/network/modules/Clustering.js @@ -0,0 +1,620 @@ +/** + * Created by Alex on 24-Feb-15. + */ + +var util = require("../../util"); + +class ClusterEngine { + constructor(body) { + this.body = body; + this.clusteredNodes = {}; + } + + + /** + * + * @param hubsize + * @param options + */ + clusterByConnectionCount(hubsize, options) { + if (hubsize === undefined) { + hubsize = this._getHubSize(); + } + else if (tyepof(hubsize) == "object") { + options = this._checkOptions(hubsize); + hubsize = this._getHubSize(); + } + + var nodesToCluster = []; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length >= hubsize) { + nodesToCluster.push(node.id); + } + } + + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node,options,{},{},true); + } + this.body.emitter.emit('_dataChanged'); + } + + + /** + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param doNotUpdateCalculationNodes + */ + clusterByNodeData(options = {}, doNotUpdateCalculationNodes = false) { + if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");} + + // check if the options object is fine, append if needed + options = this._checkOptions(options); + + var childNodesObj = {}; + var childEdgesObj = {} + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + } + + this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); + } + + + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param doNotUpdateCalculationNodes + */ + clusterOutliers(options, doNotUpdateCalculationNodes) { + options = this._checkOptions(options); + var clusters = []; + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var childNodesObj = {}; + var childEdgesObj = {}; + var nodeId = this.body.nodeIndices[i]; + if (this.body.nodes[nodeId].edges.length == 1) { + var edge = this.body.nodes[nodeId].edges[0]; + var childNodeId = this._getConnectedId(edge, nodeId); + if (childNodeId != nodeId) { + if (options.joinCondition === undefined) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + else { + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + clonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + clusters.push({nodes:childNodesObj, edges:childEdgesObj}) + } + } + } + + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, true) + } + + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit('_dataChanged'); + } + } + + /** + * + * @param nodeId + * @param options + * @param doNotUpdateCalculationNodes + */ + clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) { + // kill conditions + if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");} + if (this.body.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");} + + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;} + if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;} + + var childNodesObj = {}; + var childEdgesObj = {} + var parentNodeId = node.id; + var parentClonedOptions = this._cloneOptions(parentNodeId); + childNodesObj[parentNodeId] = node; + + // collect the nodes that will be in the cluster + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + var childNodeId = this._getConnectedId(edge, parentNodeId); + + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + } + else { + childEdgesObj[edge.id] = edge; + } + } + + this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); + } + + + /** + * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes. + * @param objId + * @param type + * @returns {{}} + * @private + */ + _cloneOptions(objId, type) { + var clonedOptions = {}; + if (type === undefined || type == 'node') { + util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); + util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); + clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + } + else { + util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + } + return clonedOptions; + } + + + /** + * This function creates the edges that will be attached to the cluster. + * + * @param childNodesObj + * @param childEdgesObj + * @param newEdges + * @param options + * @private + */ + _createClusterEdges (childNodesObj, childEdgesObj, newEdges, options) { + var edge, childNodeId, childNode; + + var childKeys = Object.keys(childNodesObj); + for (var i = 0; i < childKeys.length; i++) { + childNodeId = childKeys[i]; + childNode = childNodesObj[childNodeId]; + + // mark all edges for removal from global and construct new edges from the cluster to others + for (var j = 0; j < childNode.edges.length; j++) { + edge = childNode.edges[j]; + childEdgesObj[edge.id] = edge; + + var otherNodeId = edge.toId; + var otherOnTo = true; + if (edge.toId != childNodeId) { + otherNodeId = edge.toId; + otherOnTo = true; + } + else if (edge.fromId != childNodeId) { + otherNodeId = edge.fromId; + otherOnTo = false; + } + + if (childNodesObj[otherNodeId] === undefined) { + var clonedOptions = this._cloneOptions(edge.id, 'edge'); + util.deepExtend(clonedOptions, options.clusterEdgeProperties); + if (otherOnTo === true) { + clonedOptions.from = options.clusterNodeProperties.id; + clonedOptions.to = otherNodeId; + } + else { + clonedOptions.from = otherNodeId; + clonedOptions.to = options.clusterNodeProperties.id; + } + clonedOptions.id = 'clusterEdge:' + util.randomUUID(); + newEdges.push(this.body.functions.createEdge(clonedOptions)) + } + } + } + } + + + /** + * This function checks the options that can be supplied to the different cluster functions + * for certain fields and inserts defaults if needed + * @param options + * @returns {*} + * @private + */ + _checkOptions(options = {}) { + if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};} + if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};} + + return options; + } + + /** + * + * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node + * @param {Object} childEdgesObj | object with edge objects, id as keys + * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} + * @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up + * @private + */ + _cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes = false) { + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length == 0) {return;} + + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();} + var clusterId = options.clusterNodeProperties.id; + + // create the new edges that will connect to the cluster + var newEdges = []; + this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + + // construct the clusterNodeProperties + var clusterNodeProperties = options.clusterNodeProperties; + if (options.processProperties !== undefined) { + // get the childNode options + var childNodesOptions = []; + for (var nodeId in childNodesObj) { + var clonedOptions = this._cloneOptions(nodeId); + childNodesOptions.push(clonedOptions); + } + + // get clusterproperties based on childNodes + var childEdgesOptions = []; + for (var edgeId in childEdgesObj) { + var clonedOptions = this._cloneOptions(edgeId, 'edge'); + childEdgesOptions.push(clonedOptions); + } + + clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); + if (!clusterNodeProperties) { + throw new Error("The processClusterProperties function does not return properties!"); + } + } + if (clusterNodeProperties.label === undefined) { + clusterNodeProperties.label = 'cluster'; + } + + + // give the clusterNode a postion if it does not have one. + var pos = undefined; + if (clusterNodeProperties.x === undefined) { + pos = this._getClusterPosition(childNodesObj); + clusterNodeProperties.x = pos.x; + clusterNodeProperties.allowedToMoveX = true; + } + if (clusterNodeProperties.x === undefined) { + if (pos === undefined) { + pos = this._getClusterPosition(childNodesObj); + } + clusterNodeProperties.y = pos.y; + clusterNodeProperties.allowedToMoveY = true; + } + + + // force the ID to remain the same + clusterNodeProperties.id = clusterId; + + + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; + + + // delete contained edges from global + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + if (this.body.edges[edgeId].via !== null) { + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = null + delete this.body.supportNodes[viaId]; + } + } + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } + } + } + + + // remove contained nodes from global + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.body.nodes[nodeId]}; + delete this.body.nodes[nodeId]; + } + } + + + // finally put the cluster node into global + this.body.nodes[clusterNodeProperties.id] = clusterNode; + + + // push new edges to global + for (var i = 0; i < newEdges.length; i++) { + this.body.edges[newEdges[i].id] = newEdges[i]; + this.body.edges[newEdges[i].id].connect(); + } + + + // create bezier nodes for smooth curves if needed + this.body.emitter.emit("_newEdgesCreated"); + + + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; + + + // wrap up + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit('_dataChanged'); + } + } + + + /** + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ + isCluster(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster; + } + else { + console.log("Node does not exist.") + return false; + } + } + + /** + * get the position of the cluster node based on what's inside + * @param {object} childNodesObj | object with node objects, id as keys + * @returns {{x: number, y: number}} + * @private + */ + _getClusterPosition(childNodesObj) { + var childKeys = Object.keys(childNodesObj); + var minX = childNodesObj[childKeys[0]].x; + var maxX = childNodesObj[childKeys[0]].x; + var minY = childNodesObj[childKeys[0]].y; + var maxY = childNodesObj[childKeys[0]].y; + var node; + for (var i = 0; i < childKeys.lenght; i++) { + node = childNodesObj[childKeys[0]]; + minX = node.x < minX ? node.x : minX; + maxX = node.x > maxX ? node.x : maxX; + minY = node.y < minY ? node.y : minY; + maxY = node.y > maxY ? node.y : maxY; + } + return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)}; + } + + + /** + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true + */ + openCluster(clusterNodeId, doNotUpdateCalculationNodes) { + // kill conditions + if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");} + if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");} + if (this.body.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return}; + + var node = this.body.nodes[clusterNodeId]; + var containedNodes = node.containedNodes; + var containedEdges = node.containedEdges; + + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId] = containedNodes[nodeId]; + // inherit position + this.body.nodes[nodeId].x = node.x; + this.body.nodes[nodeId].y = node.y; + + // inherit speed + this.body.nodes[nodeId].vx = node.vx; + this.body.nodes[nodeId].vy = node.vy; + + delete this.clusteredNodes[nodeId]; + } + } + + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId] = containedEdges[edgeId]; + this.body.edges[edgeId].connect(); + var edge = this.body.edges[edgeId]; + if (edge.connected === false) { + if (this.clusteredNodes[edge.fromId] !== undefined) { + this._connectEdge(edge, edge.fromId, true); + } + if (this.clusteredNodes[edge.toId] !== undefined) { + this._connectEdge(edge, edge.toId, false); + } + } + } + } + + this.body.emitter.emit("_newEdgesCreated",containedEdges); + + + var edgeIds = []; + for (var i = 0; i < node.edges.length; i++) { + edgeIds.push(node.edges[i].id); + } + + // remove edges in clusterNode + for (var i = 0; i < edgeIds.length; i++) { + var edge = this.body.edges[edgeIds[i]]; + // if the edge should have been connected to a contained node + if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) { + // the node in the from array was contained in the cluster + if (this.body.nodes[edge.fromArray[0].id] !== undefined) { + this._connectEdge(edge, edge.fromArray[0].id, true); + } + } + else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) { + // the node in the to array was contained in the cluster + if (this.body.nodes[edge.toArray[0].id] !== undefined) { + this._connectEdge(edge, edge.toArray[0].id, false); + } + } + else { + var edgeId = edgeIds[i]; + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = null + delete this.body.supportNodes[viaId]; + } + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } + } + + // remove clusterNode + delete this.body.nodes[clusterNodeId]; + + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit('_dataChanged'); + } + } + + + + /** + * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to + * is currently residing in cluster B + * @param edge + * @param nodeId + * @param from + * @private + */ + _connectEdge(edge, nodeId, from) { + var clusterStack = this._getClusterStack(nodeId); + if (from == true) { + edge.from = clusterStack[clusterStack.length - 1]; + edge.fromId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop() + edge.fromArray = clusterStack; + } + else { + edge.to = clusterStack[clusterStack.length - 1]; + edge.toId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.toArray = clusterStack; + } + edge.connect(); + } + + /** + * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node + * @param nodeId + * @returns {Array} + * @private + */ + _getClusterStack(nodeId) { + var stack = []; + var max = 100; + var counter = 0; + + while (this.clusteredNodes[nodeId] !== undefined && counter < max) { + stack.push(this.clusteredNodes[nodeId].node); + nodeId = this.clusteredNodes[nodeId].clusterId; + counter++; + } + stack.push(this.body.nodes[nodeId]); + return stack; + } + + + /** + * Get the Id the node is connected to + * @param edge + * @param nodeId + * @returns {*} + * @private + */ + _getConnectedId(edge, nodeId) { + if (edge.toId != nodeId) { + return edge.toId; + } + else if (edge.fromId != nodeId) { + return edge.fromId; + } + else { + return edge.fromId; + } + } + + /** + * 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() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; + + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length > largestHub) { + largestHub = node.edges.length; + } + average += node.edges.length; + averageSquared += Math.pow(node.edges.length,2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; + + var variance = averageSquared - Math.pow(average,2); + var standardDeviation = Math.sqrt(variance); + + var hubThreshold = Math.floor(average + 2*standardDeviation); + + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; + } + + return hubThreshold; + }; + +} + + +export { ClusterEngine }; diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js new file mode 100644 index 00000000..a7194e15 --- /dev/null +++ b/lib/network/modules/PhysicsEngine.js @@ -0,0 +1,419 @@ +/** + * Created by Alex on 2/23/2015. + */ + +import {BarnesHutSolver} from "./components/physics/BarnesHutSolver"; +import {Repulsion} from "./components/physics/RepulsionSolver"; +import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsionSolver"; + +import {SpringSolver} from "./components/physics/SpringSolver"; +import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver"; + +import {CentralGravitySolver} from "./components/physics/CentralGravitySolver"; + +var util = require('../../util'); + + +class PhysicsEngine { + constructor(body, options) { + this.body = body; + this.physicsBody = {calculationNodes: {}, calculationNodeIndices:[], forces: {}, velocities: {}}; + this.scale = 1; + this.viewFunction = undefined; + + this.body.emitter.on("_setScale", (scale) => this.scale = scale); + + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.renderTimer == undefined; + + this.stabilized = false; + this.stabilizationIterations = 0; + + // default options + this.options = { + barnesHut: { + thetaInverted: 1 / 0.5, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.3, + springLength: 95, + springConstant: 0.04, + damping: 0.09 + }, + repulsion: { + centralGravity: 0.0, + springLength: 200, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + hierarchicalRepulsion: { + centralGravity: 0.0, + springLength: 100, + springConstant: 0.01, + nodeDistance: 150, + damping: 0.09 + }, + model: 'BarnesHut', + timestep: 0.5, + maxVelocity: 50, + minVelocity: 0.1, // px/s + stabilization: { + enabled: true, + iterations: 1000, // maximum number of iteration to stabilize + updateInterval: 100, + onlyDynamicEdges: false, + zoomExtent: true + } + } + + this.setOptions(options); + } + + setOptions(options) { + if (options !== undefined) { + if (typeof options.stabilization == 'boolean') { + options.stabilization = { + enabled: options.stabilization + } + } + util.deepExtend(this.options, options); + } + this.init(); + } + + + init() { + var options; + if (this.options.model == "repulsion") { + options = this.options.repulsion; + this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } + else if (this.options.model == "hierarchicalRepulsion") { + options = this.options.hierarchicalRepulsion; + this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); + this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); + } + else { // barnesHut + options = this.options.barnesHut; + this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } + + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; + } + + startSimulation() { + this.stabilized = false; + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } + else { + this.runSimulation(); + } + } + + runSimulation() { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + } + + simulationStep() { + // check if the physics have settled + var startTime = Date.now(); + this.physicsTick(); + var physicsTime = Date.now() - startTime; + + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { + this.physicsTick(); + + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } + + if (this.stabilized === true) { + if (this.stabilizationIterations > 1) { + // trigger the "stabilized" event. + // The event is triggered on the next tick, to prevent the case that + // it is fired while initializing the Network, in which case you would not + // be able to catch it + var me = this; + var params = { + iterations: this.stabilizationIterations + }; + this.stabilizationIterations = 0; + this.startedStabilization = false; + setTimeout(function () { + me.body.emitter.emit("stabilized", params); + }, 0); + } + else { + this.stabilizationIterations = 0; + } + this.body.emitter.emit("_stopRendering"); + } + } + + /** + * A single simulation step (or "tick") in the physics simulation + * + * @private + */ + physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); + + // determine if the network has stabilzied + if (this.stabilized === true) { + this.revert(); + } + else { + // this is here to ensure that there is no start event when the network is already stable. + if (this.startedStabilization == false) { + this.body.emitter.emit("startStabilizing"); + this.startedStabilization = true; + } + } + + this.stabilizationIterations++; + } + } + + /** + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.body.nodes with the support nodes. + * + * @private + */ + _updateCalculationNodes() { + this.physicsBody.calculationNodes = {}; + this.physicsBody.forces = {}; + this.physicsBody.calculationNodeIndices = []; + + for (let i = 0; i < this.body.nodeIndices.length; i++) { + let nodeId = this.body.nodeIndices[i]; + this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId]; + } + + // if support nodes are used, we have them here + var supportNodes = this.body.supportNodes; + for (let i = 0; i < this.body.supportNodeIndices.length; i++) { + let supportNodeId = this.body.supportNodeIndices[i]; + if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) { + this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + } + else { + console.error("Support node detected that does not have an edge!") + } + } + + this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes); + for (let i = 0; i < this.physicsBody.calculationNodeIndices.length; i++) { + let nodeId = this.physicsBody.calculationNodeIndices[i]; + this.physicsBody.forces[nodeId] = {x:0,y:0}; + + // forces can be reset because they are recalculated. Velocities have to persist. + if (this.physicsBody.velocities[nodeId] === undefined) { + this.physicsBody.velocities[nodeId] = {x:0,y:0}; + } + } + + // clean deleted nodes from the velocity vector + for (let nodeId in this.physicsBody.velocities) { + if (this.physicsBody.calculationNodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } + } + + + revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.physicsBody.calculationNodes; + var velocities = this.physicsBody.velocities; + + for (let i = 0; i < nodeIds.length; i++) { + let nodeId = nodeIds[i]; + if (nodes[nodeId] !== undefined) { + velocities[nodeId].x = this.previousStates[nodeId].vx; + velocities[nodeId].y = this.previousStates[nodeId].vy; + nodes[nodeId].x = this.previousStates[nodeId].x; + nodes[nodeId].y = this.previousStates[nodeId].y; + } + else { + delete this.previousStates[nodeId]; + } + } + } + + moveNodes() { + var nodesPresent = false; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var maxVelocity = this.options.maxVelocity === 0 ? 1e9 : this.options.maxVelocity; + var stabilized = true; + var vminCorrected = this.options.minVelocity / Math.max(this.scale,0.05); + + for (let i = 0; i < nodeIndices.length; i++) { + let nodeId = nodeIndices[i]; + let nodeVelocity = this._performStep(nodeId, maxVelocity); + // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized + stabilized = nodeVelocity < vminCorrected && stabilized === true; + nodesPresent = true; + } + + + if (nodesPresent == true) { + if (vminCorrected > 0.5*this.options.maxVelocity) { + return false; + } + else { + return stabilized; + } + } + return true; + } + + _performStep(nodeId,maxVelocity) { + var node = this.physicsBody.calculationNodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; + + // store the state so we can revert + this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y}; + + if (!node.xFixed) { + let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force + let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration + velocities[nodeId].x += ax * timestep; // velocity + velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x; + node.x += velocities[nodeId].x * timestep; // position + } + else { + forces[nodeId].x = 0; + velocities[nodeId].x = 0; + } + + if (!node.yFixed) { + let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force + let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration + velocities[nodeId].y += ay * timestep; // velocity + velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y; + node.y += velocities[nodeId].y * timestep; // position + } + else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; + } + + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2)); + return totalVelocity; + } + + calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); + } + + + + + + + + + + /** + * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization + * because only the supportnodes for the smoothCurves have to settle. + * + * @private + */ + _freezeNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x != null && nodes[id].y != null) { + nodes[id].fixedData.x = nodes[id].xFixed; + nodes[id].fixedData.y = nodes[id].yFixed; + nodes[id].xFixed = true; + nodes[id].yFixed = true; + } + } + } + } + + /** + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * + * @private + */ + _restoreFrozenNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].fixedData.x != null) { + nodes[id].xFixed = nodes[id].fixedData.x; + nodes[id].yFixed = nodes[id].fixedData.y; + } + } + } + } + + /** + * Find a stable position for all nodes + * @private + */ + stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); + } + this.stabilizationSteps = 0; + + setTimeout(this._stabilizationBatch.bind(this),0); + } + + _stabilizationBatch() { + var count = 0; + while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { + this.physicsTick(); + this.stabilizationSteps++; + count++; + } + + if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { + this.body.emitter.emit("stabilizationProgress", {steps: this.stabilizationSteps, total: this.options.stabilization.iterations}); + setTimeout(this._stabilizationBatch.bind(this),0); + } + else { + this._finalizeStabilization(); + } + } + + _finalizeStabilization() { + if (this.options.stabilization.zoomExtent == true) { + this.body.emitter.emit("zoomExtent", {duration:0}); + } + + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); + } + + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + } + +} + +export {PhysicsEngine}; \ No newline at end of file diff --git a/lib/network/modules/TouchEventHandler.js b/lib/network/modules/TouchEventHandler.js new file mode 100644 index 00000000..b5b07473 --- /dev/null +++ b/lib/network/modules/TouchEventHandler.js @@ -0,0 +1,460 @@ +/** + * Created by Alex on 2/27/2015. + * + */ + +import {SelectionHandler} from "./components/SelectionHandler" +var util = require('../../util'); + +class TouchEventHandler { + constructor(body) { + this.body = body; + + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); + + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.pointerPosition = {x:0,y:0}; + + this.scale = 1.0; + this.body.emitter.on("_setScale", (scale) => this.scale = scale); + + this.selectionHandler = new SelectionHandler(body); + } + + setCanvas(canvas) { + this.canvas = canvas; + this.selectionHandler.setCanvas(canvas); + } + + /** + * Get the pointer location from a touch location + * @param {{pageX: Number, pageY: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ + getPointer(touch) { + return { + x: touch.pageX - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.pageY - util.getAbsoluteTop(this.canvas.frame.canvas) + }; + } + + + /** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + onTouch(event) { + if (new Date().valueOf() - this.touchTime > 100) { + this.drag.pointer = this.getPointer(event.gesture.center); + this.drag.pinched = false; + this.pinch.scale = this.scale; + + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); + } + } + + /** + * handle tap/click event: select/unselect a node + * @private + */ + onTap(event) { + console.log("tap",event) + var pointer = this.getPointer(event.gesture.center); + this.pointerPosition = pointer; + this.selectionHandler.selectOnPoint(pointer); + } + + /** + * handle drag start event + * @private + */ + + /** + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ + onDragStart(event) { + // in case the touch event was triggered on an external div, do the initial touch now. + //if (this.drag.pointer === undefined) { + // this.onTouch(event); + //} + // + //var node = this._getNodeAt(this.drag.pointer); + //// note: drag.pointer is set in onTouch to get the initial touch location + // + //this.drag.dragging = true; + //this.drag.selection = []; + //this.drag.translation = this._getTranslation(); + //this.drag.nodeId = null; + //this.draggingNodes = false; + // + //if (node != null && this.constants.dragNodes == true) { + // this.draggingNodes = true; + // this.drag.nodeId = node.id; + // // select the clicked node if not yet selected + // if (!node.isSelected()) { + // this._selectObject(node, false); + // } + // + // this.emit("dragStart", {nodeIds: this.getSelection().nodes}); + // + // // create an array with the selected nodes and their original location and status + // for (var objectId in this.selectionObj.nodes) { + // if (this.selectionObj.nodes.hasOwnProperty(objectId)) { + // var object = this.selectionObj.nodes[objectId]; + // var s = { + // id: object.id, + // node: object, + // + // // store original x, y, xFixed and yFixed, make the node temporarily Fixed + // x: object.x, + // y: object.y, + // xFixed: object.xFixed, + // yFixed: object.yFixed + // }; + // + // object.xFixed = true; + // object.yFixed = true; + // + // this.drag.selection.push(s); + // } + // } + //} + } + + + /** + * handle drag event + * @private + */ + onDrag(event) { + //if (this.drag.pinched) { + // return; + //} + // + //// remove the focus on node if it is focussed on by the focusOnNode + //this.releaseNode(); + // + //var pointer = this.getPointer(event.gesture.center); + //var me = this; + //var drag = this.drag; + //var selection = drag.selection; + //if (selection && selection.length && this.constants.dragNodes == true) { + // // calculate delta's and new location + // var deltaX = pointer.x - drag.pointer.x; + // var deltaY = pointer.y - drag.pointer.y; + // + // // update position of all selected nodes + // selection.forEach(function (s) { + // var node = s.node; + // + // if (!s.xFixed) { + // node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); + // } + // + // if (!s.yFixed) { + // node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); + // } + // }); + // + // + // // start _animationStep if not yet running + // if (!this.moving) { + // this.moving = true; + // this.start(); + // } + //} + //else { + // // move the network + // if (this.constants.dragNetwork == true) { + // // if the drag was not started properly because the click started outside the network div, start it now. + // if (this.drag.pointer === undefined) { + // this._handleDragStart(event); + // return; + // } + // var diffX = pointer.x - this.drag.pointer.x; + // var diffY = pointer.y - this.drag.pointer.y; + // + // this._setTranslation( + // this.drag.translation.x + diffX, + // this.drag.translation.y + diffY + // ); + // this._redraw(); + // } + //} + } + + + /** + * handle drag start event + * @private + */ + onDragEnd(event) { + //this.drag.dragging = false; + //var selection = this.drag.selection; + //if (selection && selection.length) { + // selection.forEach(function (s) { + // // restore original xFixed and yFixed + // s.node.xFixed = s.xFixed; + // s.node.yFixed = s.yFixed; + // }); + // this.moving = true; + // this.start(); + //} + //else { + // this._redraw(); + //} + //if (this.draggingNodes == false) { + // this.emit("dragEnd", {nodeIds: []}); + //} + //else { + // this.emit("dragEnd", {nodeIds: this.getSelection().nodes}); + //} + } + + /** + * handle doubletap event + * @private + */ + onDoubleTap(event) { + //var pointer = this.getPointer(event.gesture.center); + //this._handleDoubleTap(pointer); + } + + + + /** + * handle long tap event: multi select nodes + * @private + */ + onHold(event) { + //var pointer = this.getPointer(event.gesture.center); + //this.pointerPosition = pointer; + //this._handleOnHold(pointer); + } + + + /** + * handle the release of the screen + * + * @private + */ + onRelease(event) { + //var pointer = this.getPointer(event.gesture.center); + //this._handleOnRelease(pointer); + } + + + /** + * Handle pinch event + * @param event + * @private + */ + onPinch(event) { + //var pointer = this.getPointer(event.gesture.center); + // + //this.drag.pinched = true; + //if (!('scale' in this.pinch)) { + // this.pinch.scale = 1; + //} + // + //// TODO: enabled moving while pinching? + //var scale = this.pinch.scale * event.gesture.scale; + //this._zoom(scale, pointer) + } + + + /** + * Zoom the network in or out + * @param {Number} scale a number around 1, and between 0.01 and 10 + * @param {{x: Number, y: Number}} pointer Position on screen + * @return {Number} appliedScale scale is limited within the boundaries + * @private + */ + _zoom(scale, pointer) { + //if (this.constants.zoomable == true) { + // var scaleOld = this._getScale(); + // if (scale < 0.00001) { + // scale = 0.00001; + // } + // if (scale > 10) { + // scale = 10; + // } + // + // var preScaleDragPointer = null; + // if (this.drag !== undefined) { + // if (this.drag.dragging == true) { + // preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + // } + // } + // // + this.canvas.frame.canvas.clientHeight / 2 + // var translation = this._getTranslation(); + // + // var scaleFrac = scale / scaleOld; + // var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + // var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + // + // this._setScale(scale); + // this._setTranslation(tx, ty); + // + // if (preScaleDragPointer != null) { + // var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + // this.drag.pointer.x = postScaleDragPointer.x; + // this.drag.pointer.y = postScaleDragPointer.y; + // } + // + // this._redraw(); + // + // if (scaleOld < scale) { + // this.emit("zoom", {direction: "+"}); + // } + // else { + // this.emit("zoom", {direction: "-"}); + // } + // + // return scale; + //} + } + + + /** + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event + * @private + */ + onMouseWheel(event) { + //// retrieve delta + //var delta = 0; + //if (event.wheelDelta) { /* IE/Opera. */ + // delta = event.wheelDelta / 120; + //} else if (event.detail) { /* Mozilla case. */ + // // In Mozilla, sign of delta is different than in IE. + // // Also, delta is multiple of 3. + // delta = -event.detail / 3; + //} + // + //// If delta is nonzero, handle it. + //// Basically, delta is now positive if wheel was scrolled up, + //// and negative, if wheel was scrolled down. + //if (delta) { + // + // // calculate the new scale + // var scale = this._getScale(); + // var zoom = delta / 10; + // if (delta < 0) { + // zoom = zoom / (1 - zoom); + // } + // scale *= (1 + zoom); + // + // // calculate the pointer location + // var gesture = hammerUtil.fakeGesture(this, event); + // var pointer = this.getPointer(gesture.center); + // + // // apply the new scale + // this._zoom(scale, pointer); + //} + // + //// Prevent default actions caused by mouse wheel. + //event.preventDefault(); + } + + + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + onMouseMove(event) { + //var gesture = hammerUtil.fakeGesture(this, event); + //var pointer = this.getPointer(gesture.center); + //var popupVisible = false; + // + //// check if the previously selected node is still selected + //if (this.popup !== undefined) { + // if (this.popup.hidden === false) { + // this._checkHidePopup(pointer); + // } + // + // // if the popup was not hidden above + // if (this.popup.hidden === false) { + // popupVisible = true; + // this.popup.setPosition(pointer.x + 3, pointer.y - 5) + // this.popup.show(); + // } + //} + // + //// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over + //if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { + // this.canvas.frame.focus(); + //} + // + //// start a timeout that will check if the mouse is positioned above an element + //if (popupVisible === false) { + // var me = this; + // var checkShow = function() { + // me._checkShowPopup(pointer); + // }; + // + // if (this.popupTimer) { + // clearInterval(this.popupTimer); // stop any running calculationTimer + // } + // if (!this.drag.dragging) { + // this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); + // } + //} + // + ///** + // * Adding hover highlights + // */ + //if (this.constants.hover == true) { + // // removing all hover highlights + // for (var edgeId in this.hoverObj.edges) { + // if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + // this.hoverObj.edges[edgeId].hover = false; + // delete this.hoverObj.edges[edgeId]; + // } + // } + // + // // adding hover highlights + // var obj = this._getNodeAt(pointer); + // if (obj == null) { + // obj = this._getEdgeAt(pointer); + // } + // if (obj != null) { + // this._hoverObject(obj); + // } + // + // // removing all node hover highlights except for the selected one. + // for (var nodeId in this.hoverObj.nodes) { + // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { + // this._blurObject(this.hoverObj.nodes[nodeId]); + // delete this.hoverObj.nodes[nodeId]; + // } + // } + // } + // this.redraw(); + //} + } +} + +export {TouchEventHandler}; diff --git a/lib/network/modules/View.js b/lib/network/modules/View.js new file mode 100644 index 00000000..58a3cb45 --- /dev/null +++ b/lib/network/modules/View.js @@ -0,0 +1,342 @@ +/** + * Created by Alex on 26-Feb-15. + */ + +var util = require('../../util'); + +class View { + constructor(body, options) { + this.body = body; + this.setOptions(options); + + this.animationSpeed = 1/this.renderRefreshRate; + this.animationEasingFunction = "easeInOutQuint"; + this.easingTime = 0; + this.sourceScale = 0; + this.targetScale = 0; + this.sourceTranslation = 0; + this.targetTranslation = 0; + this.lockedOnNodeId = null; + this.lockedOnNodeOffset = null; + this.touchTime = 0; + + this.translation = {x: 0, y: 0}; + this.scale = 1.0; + + this.viewFunction = undefined; + + this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); + this.body.emitter.on("_setScale", (scale) => this.scale = scale); + this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); + this.body.emitter.on("animationFinished", () => {this.body.emitter.emit("_stopRendering");}); + this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); + } + + + setOptions(options = {}) { + this.options = options; + } + + setCanvas(canvas) { + this.canvas = canvas; + } + + // zoomExtent + /** + * Find the center position of the network + * @private + */ + _getRange(specificNodes = []) { + var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; + if (specificNodes.length > 0) { + for (var i = 0; i < specificNodes.length; i++) { + node = this.body.nodes[specificNodes[i]]; + if (minX > (node.boundingBox.left)) { + minX = node.boundingBox.left; + } + if (maxX < (node.boundingBox.right)) { + maxX = node.boundingBox.right; + } + if (minY > (node.boundingBox.bottom)) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < (node.boundingBox.top)) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive + } + } + else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (minX > (node.boundingBox.left)) { + minX = node.boundingBox.left; + } + if (maxX < (node.boundingBox.right)) { + maxX = node.boundingBox.right; + } + if (minY > (node.boundingBox.bottom)) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < (node.boundingBox.top)) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive + } + } + } + + if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { + minY = 0, maxY = 0, minX = 0, maxX = 0; + } + return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + } + + + /** + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private + */ + _findCenter(range) { + return {x: (0.5 * (range.maxX + range.minX)), + y: (0.5 * (range.maxY + range.minY))}; + } + + + /** + * This function zooms out to fit all data on screen based on amount of nodes + * @param {Object} + * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; + * @param {Boolean} [disableStart] | If true, start is not called. + */ + zoomExtent(options = {nodes:[]}, initialZoom = false) { + var range; + var zoomLevel; + + if (initialZoom == true) { + // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. + var positionDefined = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.predefinedPosition == true) { + positionDefined += 1; + } + } + } + if (positionDefined > 0.5 * this.body.nodeIndices.length) { + this.zoomExtent(options,false); + return; + } + + range = this._getRange(options.nodes); + + var numberOfNodes = this.body.nodeIndices.length; + if (this.options.smoothCurves == true) { + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + else { + zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + + // correct for larger canvasses. + var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); + zoomLevel *= factor; + } + else { + this.body.emitter.emit("_redrawHidden"); + range = this._getRange(options.nodes); + var xDistance = Math.abs(range.maxX - range.minX) * 1.1; + var yDistance = Math.abs(range.maxY - range.minY) * 1.1; + + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; + } + + if (zoomLevel > 1.0) { + zoomLevel = 1.0; + } + + + var center = this._findCenter(range); + var animationOptions = {position: center, scale: zoomLevel, animation: options}; + this.moveTo(animationOptions); + } + + // animation + + /** + * Center a node in view. + * + * @param {Number} nodeId + * @param {Number} [options] + */ + focusOnNode(nodeId, options = {}) { + if (this.body.nodes[nodeId] !== undefined) { + var nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y}; + options.position = nodePosition; + options.lockedOnNode = nodeId; + + this.moveTo(options) + } + else { + console.log("Node: " + nodeId + " cannot be found."); + } + } + + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.scale = Number // scale to move to + * | options.position = {x:Number, y:Number} // position to move to + * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to + */ + moveTo(options) { + if (options === undefined) { + options = {}; + return; + } + if (options.offset === undefined) {options.offset = {x: 0, y: 0}; } + if (options.offset.x === undefined) {options.offset.x = 0; } + if (options.offset.y === undefined) {options.offset.y = 0; } + if (options.scale === undefined) {options.scale = this.scale; } + if (options.position === undefined) {options.position = this.translation;} + if (options.animation === undefined) {options.animation = {duration:0}; } + if (options.animation === false ) {options.animation = {duration:0}; } + if (options.animation === true ) {options.animation = {}; } + if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration + if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function + + this.animateView(options); + } + + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.time = Number // animation time in milliseconds + * | options.scale = Number // scale to animate to + * | options.position = {x:Number, y:Number} // position to animate to + * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, + * // easeInCubic, easeOutCubic, easeInOutCubic, + * // easeInQuart, easeOutQuart, easeInOutQuart, + * // easeInQuint, easeOutQuint, easeInOutQuint + */ + animateView(options) { + if (options === undefined) { + return; + } + this.animationEasingFunction = options.animation.easingFunction; + // release if something focussed on the node + this.releaseNode(); + if (options.locked == true) { + this.lockedOnNodeId = options.lockedOnNode; + this.lockedOnNodeOffset = options.offset; + } + + // forcefully complete the old animation if it was still running + if (this.easingTime != 0) { + this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + } + + this.sourceScale = this.scale; + this.sourceTranslation = this.translation; + this.targetScale = options.scale; + + // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw + // but at least then we'll have the target transition + this.body.emitter.emit("_setScale",this.targetScale); + var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight}); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - options.position.x, + y: viewCenter.y - options.position.y + }; + this.targetTranslation = { + x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, + y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y + }; + + // if the time is set to 0, don't do an animation + if (options.animation.duration == 0) { + if (this.lockedOnNodeId != null) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + } + else { + this.body.emitter.emit("_setScale", this.targetScale);; + this.body.emitter.emit("_setTranslation", this.targetTranslation); + this.body.emitter.emit("_requestRedraw"); + } + } + else { + this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's + this.animationEasingFunction = options.animation.easingFunction; + + + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + } + + /** + * used to animate smoothly by hijacking the redraw function. + * @private + */ + _lockedRedraw() { + var nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y}; + var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - nodePosition.x, + y: viewCenter.y - nodePosition.y + }; + var sourceTranslation = this.translation; + var targetTranslation = { + x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, + y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y + }; + + this.body.emitter.emit("_setTranslation", targetTranslation); + } + + releaseNode() { + if (this.lockedOnNodeId !== undefined) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + } + } + + /** + * + * @param easingTime + * @private + */ + _transitionRedraw(finished = false) { + this.easingTime += this.animationSpeed; + this.easingTime = finished === true ? 1.0 : this.easingTime; + + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + + this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress); + this.body.emitter.emit("_setTranslation", { + x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, + y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress + }); + + // cleanup + if (this.easingTime >= 1.0) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != null) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + } + this.body.emitter.emit("animationFinished"); + } + }; + + +} + +export {View}; \ No newline at end of file diff --git a/lib/network/modules/components/SelectionHandler.js b/lib/network/modules/components/SelectionHandler.js new file mode 100644 index 00000000..800395ba --- /dev/null +++ b/lib/network/modules/components/SelectionHandler.js @@ -0,0 +1,640 @@ +/** + * Created by Alex on 2/27/2015. + */ + +var Node = require("../../Node"); + +class SelectionHandler { + constructor(body) { + this.body = body; + this.selectionObj = {nodes:[], edges:[]}; + + this.options = { + select: true, + selectConnectedEdges: true + } + } + + setCanvas(canvas) { + this.canvas = canvas; + } + + + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + selectOnPoint(pointer) { + if (this.options.select === true) { + if (this._getSelectedObjectCount() > 0) {this._unselectAll();} + + this.selectObject(pointer); + this._generateClickEvent(pointer); + + this.body.emitter.emit("_requestRedraw"); + } + } + + _generateClickEvent(pointer) { + var properties = this.getSelection(); + properties['pointer'] = { + DOM: {x: pointer.x, y: pointer.y}, + canvas: this.canvas.DOMtoCanvas(pointer) + } + this.body.emitter.emit("click", properties); + } + + selectObject(pointer) { + var obj = this._getNodeAt(pointer); + if (obj != null) { + if (this.options.selectConnectedEdges === true) { + this._selectConnectedEdges(obj); + } + } + else { + obj = this._getEdgeAt(pointer); + } + + if (obj !== null) { + obj.select(); + this._addToSelection(obj); + this.body.emitter.emit('selected', this.getSelection()) + } + return obj; + } + + + /** + * + * @param object + * @param overlappingNodes + * @private + */ + _getNodesOverlappingWith(object, overlappingNodes) { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } + } + + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + this._getNodesOverlappingWith(object,overlappingNodes); + return overlappingNodes; + } + + + /** + * 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(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x, + top: canvasPos.y, + right: canvasPos.x, + bottom: canvasPos.y + }; + } + + + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + _getNodeAt(pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } + else { + return null; + } + } + + + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } + } + + + /** + * 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 + */ + _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object,overlappingEdges); + return overlappingEdges; + } + + + /** + * 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(pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } + else { + return null; + } + } + + + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } + else { + this.selectionObj.edges[obj.id] = obj; + } + } + + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } + else { + this.hoverObj.edges[obj.id] = obj; + } + } + + + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } + else { + delete this.selectionObj.edges[obj.id]; + } + } + + /** + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + _unselectAll(doNotTrigger = false) { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } + + this.selectionObj = {nodes:{},edges:{}}; + + if (doNotTrigger == false) { + this.body.emitter.emit('select', this.getSelection()); + } + } + + + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; + } + + /** + * return the selected node + * + * @returns {number} + * @private + */ + _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return null; + } + + /** + * return the selected edge + * + * @returns {number} + * @private + */ + _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return null; + } + + + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + } + + + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + _getSelectedObjectCount() { + var count = 0; + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + } + + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + _selectionIsEmpty() { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; + } + + + /** + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + _clusterInSelection() { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } + } + } + return false; + } + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); + } + } + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } + } + + + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } + } + + + + + + + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + _blurObject(object) { + if (object.hover == true) { + object.hover = false; + this.body.emitter.emit("blurNode",{node:object.id}); + } + } + + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + _hoverObject(object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.body.emitter.emit("hoverNode",{node:object.id}); + } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); + } + } + + + + + /** + * handles the selection part of the double tap and opens a cluster if needed + * + * @param {Object} pointer + * @private + */ + _handleDoubleTap(pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), + "y" : this._YconvertDOMtoCanvas(pointer.y)}; + this.openCluster(node); + } + var properties = this.getSelection(); + properties['pointer'] = { + DOM: {x: pointer.x, y: pointer.y}, + canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} + } + this.body.emitter.emit("doubleClick", properties); + } + + + /** + * Handle the onHold selection part + * + * @param pointer + * @private + */ + _handleOnHold(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node,true); + } + else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,true); + } + } + this._requestRedraw(); + } + + + /** + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection + */ + getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return {nodes:nodeIds, edges:edgeIds}; + } + + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + getSelectedNodes() { + var idArray = []; + if (this.options.select == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + } + return idArray + } + + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + getSelectedEdges() { + var idArray = []; + if (this.options.select == true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } + } + } + return idArray; + } + + + /** + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] + */ + selectNodes(selection, highlightEdges) { + 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.body.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + this._selectObject(node,true,true,highlightEdges,true); + } + this.redraw(); + } + + + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + selectEdges(selection) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError('Edge with id "' + id + '" not found'); + } + this._selectObject(edge,true,true,false,true); + } + this.redraw(); + } + + /** + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + _updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } + } + } +} + +export {SelectionHandler}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/BarnesHutSolver.js b/lib/network/modules/components/physics/BarnesHutSolver.js new file mode 100644 index 00000000..5f555eb5 --- /dev/null +++ b/lib/network/modules/components/physics/BarnesHutSolver.js @@ -0,0 +1,446 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class BarnesHutSolver { + constructor(body, physicsBody, options) { + this.body = body; + this.physicsBody = physicsBody; + this.barnesHutTree; + this.setOptions(options); + } + + setOptions(options) { + this.options = options; + } + + + /** + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * + * @private + */ + solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var nodeCount = nodeIndices.length; + + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + + // for debugging + this.barnesHutTree = barnesHutTree; + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } + } + } + } + + + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; + + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // BarnesHutSolver condition + // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.options.thetaInverted) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } + else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } + else { // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } + } + } + } + } + + + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize + + + var minimumTreeSize = 1e-5; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); + + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: {x: 0, y: 0}, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: {data: null}, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); + + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } + + // make global + return barnesHutTree + } + + + /** + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private + */ + _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; + + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; + + } + + + /** + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private + */ + _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { // in NW + this._placeInRegion(parentBranch, node, "NW"); + } + else { // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } + else { // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { // in NE + this._placeInRegion(parentBranch, node, "NE"); + } + else { // in SE + this._placeInRegion(parentBranch, node, "SE"); + } + } + } + + + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && + parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); + } + else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); + } + break; + case 4: // place in branch + this._placeInTree(parentBranch.children[region], node); + break; + } + } + + + /** + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch + * @private + */ + _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; + } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } + } + + + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } + + + parentBranch.children[region] = { + centerOfMass: {x: 0, y: 0}, + mass: 0, + range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY}, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: {data: null}, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; + } + + + + + //--------------------------- DEBUGGING BELOW ---------------------------// + + + /** + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private + */ + _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { + + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root, ctx, color); + } + } + + + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } + + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); + + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ + } +} + + +export {BarnesHutSolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/CentralGravitySolver.js b/lib/network/modules/components/physics/CentralGravitySolver.js new file mode 100644 index 00000000..c288985f --- /dev/null +++ b/lib/network/modules/components/physics/CentralGravitySolver.js @@ -0,0 +1,40 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class CentralGravitySolver { + constructor(body, physicsBody, options) { + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + setOptions(options) { + this.options = options; + } + + solve() { + var dx, dy, distance, node, i; + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + var gravity = this.options.centralGravity; + var gravityForce = 0; + + for (i = 0; i < nodeIndices.length; i++) { + let nodeId = nodeIndices[i]; + node = nodes[nodeId]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + gravityForce = (distance == 0) ? 0 : (gravity / distance); + forces[nodeId].x = dx * gravityForce; + forces[nodeId].y = dy * gravityForce; + } + } +} + + +export {CentralGravitySolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/HierarchicalRepulsionSolver.js b/lib/network/modules/components/physics/HierarchicalRepulsionSolver.js new file mode 100644 index 00000000..2a7db867 --- /dev/null +++ b/lib/network/modules/components/physics/HierarchicalRepulsionSolver.js @@ -0,0 +1,73 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class HierarchicalRepulsionSolver { + constructor(body, physicsBody, options) { + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + setOptions(options) { + this.options = options; + } + + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; + + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + // nodes only affect nodes on their level + if (node1.level == node2.level) { + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); + } + else { + repulsingForce = 0; + } + // normalize force with + if (distance == 0) { + distance = 0.01; + } + else { + repulsingForce = repulsingForce / distance; + } + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } + } +} + + +export {HierarchicalRepulsionSolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/HierarchicalSpringSolver.js b/lib/network/modules/components/physics/HierarchicalSpringSolver.js new file mode 100644 index 00000000..745a91bd --- /dev/null +++ b/lib/network/modules/components/physics/HierarchicalSpringSolver.js @@ -0,0 +1,104 @@ +/** + * Created by Alex on 2/25/2015. + */ + +class HierarchicalSpringSolver { + constructor(body, physicsBody, options) { + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + setOptions(options) { + this.options = options; + } + + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + solve() { + var edgeLength, edge, edgeId; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + // initialize the spring force counters + for (let i = 0; i < nodeIndices.length; i++) { + let nodeId = nodeIndices[i]; + forces[nodeId].springFx = 0; + forces[nodeId].springFy = 0; + } + + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected === true) { + edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; + + dx = (edge.from.x - edge.to.x); + dy = (edge.from.y - edge.to.y); + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + if (edge.to.level != edge.from.level) { + forces[edge.toId].springFx -= fx; + forces[edge.toId].springFy -= fy; + forces[edge.fromId].springFx += fx; + forces[edge.fromId].springFy += fy; + } + else { + let factor = 0.5; + forces[edge.toId].x -= factor*fx; + forces[edge.toId].y -= factor*fy; + forces[edge.fromId].x += factor*fx; + forces[edge.fromId].y += factor*fy; + } + } + } + } + + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (let i = 0; i < nodeIndices.length; i++) { + let nodeId = nodeIndices[i]; + springFx = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFx)); + springFy = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFy)); + + forces[nodeId].x += springFx; + forces[nodeId].y += springFy; + } + + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (let i = 0; i < nodeIndices.length; i++) { + let nodeId = nodeIndices[i]; + totalFx += forces[nodeId].x; + totalFy += forces[nodeId].y; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; + + for (let i = 0; i < nodeIndices.length; i++) { + let nodeId = nodeIndices[i]; + forces[nodeId].x -= correctionFx; + forces[nodeId].y -= correctionFy; + } + } + +} + +export {HierarchicalSpringSolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/RepulsionSolver.js b/lib/network/modules/components/physics/RepulsionSolver.js new file mode 100644 index 00000000..97c29b7d --- /dev/null +++ b/lib/network/modules/components/physics/RepulsionSolver.js @@ -0,0 +1,75 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class RepulsionSolver { + constructor(body, physicsBody, options) { + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + setOptions(options) { + this.options = options; + } + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + + var nodes = this.physicsBody.calculationNodes; + var nodeIndices = this.physicsBody.calculationNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // approximation constants + var a = (-2 / 3) / nodeDistance; + var b = 4 / 3; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (let i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (let j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. + if (distance == 0) { + distance = 0.1*Math.random(); + dx = distance; + } + + if (distance < 2 * nodeDistance) { + if (distance < 0.5 * nodeDistance) { + repulsingForce = 1.0; + } + else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) + } + repulsingForce = repulsingForce / distance; + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } + } +} + + +export {RepulsionSolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/SpringSolver.js b/lib/network/modules/components/physics/SpringSolver.js new file mode 100644 index 00000000..2905667d --- /dev/null +++ b/lib/network/modules/components/physics/SpringSolver.js @@ -0,0 +1,80 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class SpringSolver { + constructor(body, physicsBody, options) { + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + setOptions(options) { + this.options = options; + } + + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + solve() { + var edgeLength, edge, edgeId; + var edges = this.body.edges; + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected === true) { + // only calculate forces if nodes are in the same sector + if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { + edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; + if (edge.via != null) { + var node1 = edge.to; + var node2 = edge.via; + var node3 = edge.from; + + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } + else { + this._calculateSpringForce(edge.from, edge.to, edgeLength); + } + } + } + } + } + } + + + /** + * This is the code actually performing the calculation for the function above. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private + */ + _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; + + dx = (node1.x - node2.x); + dy = (node1.y - node2.y); + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + this.physicsBody.forces[node1.id].x += fx; + this.physicsBody.forces[node1.id].y += fy; + this.physicsBody.forces[node2.id].x -= fx; + this.physicsBody.forces[node2.id].y -= fy; + } +} + +export {SpringSolver}; \ No newline at end of file diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 25273b94..cc3fef4f 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -229,9 +229,7 @@ Core.prototype.setOptions = function (options) { } // propagate options to all components - this.components.forEach(function (component) { - component.setOptions(options); - }); + this.components.forEach(component => component.setOptions(options)); // TODO: remove deprecation error one day (deprecated since version 0.8.0) if (options && options.order) { @@ -285,9 +283,7 @@ Core.prototype.destroy = function () { this.hammer = null; // give all components the opportunity to cleanup - this.components.forEach(function (component) { - component.destroy(); - }); + this.components.forEach(component => component.destroy()); this.body = null; }; @@ -449,9 +445,7 @@ Core.prototype.clear = function(what) { // clear options of timeline and of each of the components if (!what || what.options) { - this.components.forEach(function (component) { - component.setOptions(component.defaultOptions); - }); + this.components.forEach(component => component.setOptions(component.defaultOptions)); this.setOptions(this.defaultOptions); // this will also do a redraw } diff --git a/lib/timeline/TimeStep.js b/lib/timeline/TimeStep.js index 8d8bc7cb..5313ace1 100644 --- a/lib/timeline/TimeStep.js +++ b/lib/timeline/TimeStep.js @@ -576,9 +576,9 @@ TimeStep.prototype.getClassName = function() { case 'hour': var hours = date.hours(); if (this.step == 4) { - hours = hours + '-' + (hours + 4); + hours = hours + '-h' + (hours + 4); } - return hours + 'h' + today(date) + even(date.hours()); + return 'h' + hours + today(date) + even(date.hours()); case 'weekday': return date.format('dddd').toLowerCase() + diff --git a/lib/timeline/component/CurrentTime.js b/lib/timeline/component/CurrentTime.js index 2cbb4f70..a97757a8 100644 --- a/lib/timeline/component/CurrentTime.js +++ b/lib/timeline/component/CurrentTime.js @@ -122,7 +122,7 @@ CurrentTime.prototype.start = function() { me.redraw(); - // start a timer to adjust for the new time + // start a renderTimer to adjust for the new time me.currentTimeTimer = setTimeout(update, interval); } diff --git a/lib/timeline/component/css/item.css b/lib/timeline/component/css/item.css index bdc67340..8b766ab5 100644 --- a/lib/timeline/component/css/item.css +++ b/lib/timeline/component/css/item.css @@ -6,7 +6,7 @@ border-width: 1px; background-color: #D5DDF6; display: inline-block; - padding: 5px; + overflow: hidden; } .vis.timeline .item.selected { @@ -50,7 +50,6 @@ } .vis.timeline .item.background { - overflow: hidden; border: none; background-color: rgba(213, 221, 246, 0.4); box-sizing: border-box; @@ -62,13 +61,11 @@ position: relative; display: inline-block; max-width: 100%; - overflow: hidden; } .vis.timeline .item.background .content { position: absolute; display: inline-block; - overflow: hidden; max-width: 100%; margin: 5px; } @@ -83,7 +80,8 @@ .vis.timeline .item .content { white-space: nowrap; - overflow: hidden; + box-sizing: border-box; + padding: 5px; } .vis.timeline .item .delete { diff --git a/lib/timeline/component/graph2d_types/bar.js b/lib/timeline/component/graph2d_types/bar.js index 3dee4b21..1ba41365 100644 --- a/lib/timeline/component/graph2d_types/bar.js +++ b/lib/timeline/component/graph2d_types/bar.js @@ -58,7 +58,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { combinedData.push({ x: processedGroupData[groupIds[i]][j].x, y: processedGroupData[groupIds[i]][j].y, - groupId: groupIds[i] + groupId: groupIds[i], + label: processedGroupData[groupIds[i]][j].label }); barPoints += 1; } @@ -114,7 +115,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg); // draw points if (group.options.drawPoints.enabled == true) { - DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); + Points.draw([combinedData[i]], group, framework, drawData.offset); + //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); } } }; diff --git a/lib/util.js b/lib/util.js index c1a1d2a2..8184f42b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -225,22 +225,24 @@ exports.selectiveNotDeepExtend = function (props, a, b) { * Deep extend an object a with the properties of object b * @param {Object} a * @param {Object} b + * @param {Boolean} protoExtend --> optional parameter. If true, the prototype values will also be extended. + * (ie. the options objects that inherit from others will also get the inherited options) * @returns {Object} */ -exports.deepExtend = function(a, b) { +exports.deepExtend = function(a, b, protoExtend) { // TODO: add support for Arrays to deepExtend if (Array.isArray(b)) { throw new TypeError('Arrays are not supported by deepExtend'); } for (var prop in b) { - if (b.hasOwnProperty(prop)) { + if (b.hasOwnProperty(prop) || protoExtend === true) { if (b[prop] && b[prop].constructor === Object) { if (a[prop] === undefined) { a[prop] = {}; } if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop]); + exports.deepExtend(a[prop], b[prop], protoExtend); } else { a[prop] = b[prop]; diff --git a/package.json b/package.json index 188ad4e3..39dbf275 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "3.10.1-SNAPSHOT", + "version": "4.0.0-SNAPSHOT", "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", "repository": { @@ -35,6 +35,9 @@ "propagating-hammerjs": "^1.2.0" }, "devDependencies": { + "6to5": "^3.5.3", + "6to5-loader": "^3.0.0", + "6to5ify": "^4.1.0", "clean-css": "latest", "gulp": "^3.8.11", "gulp-concat": "^2.4.3", diff --git a/test/DataSet.test.js b/test/DataSet.test.js index 0def1526..23344a02 100644 --- a/test/DataSet.test.js +++ b/test/DataSet.test.js @@ -1,7 +1,9 @@ var assert = require('assert'); -var moment = require('moment'); -var DataSet = require('../lib/DataSet'); -var Queue = require('../lib/Queue'); +var vis = require('../dist/vis'); +var moment = vis.moment; +var DataSet = vis.DataSet; +var Queue = vis.Queue; +// TODO: test the source code immediately, but this is ES6 var now = new Date(); diff --git a/test/DataView.test.js b/test/DataView.test.js index bb8ff1f9..17aa7680 100644 --- a/test/DataView.test.js +++ b/test/DataView.test.js @@ -1,7 +1,9 @@ var assert = require('assert'); -var moment = require('moment'); -var DataSet = require('../lib/DataSet'); -var DataView = require('../lib/DataView'); +var vis = require('../dist/vis'); +var moment = vis.moment; +var DataSet = vis.DataSet; +var DataView = vis.DataView; +// TODO: test the source code immediately, but this is ES6 // TODO: improve DataView tests, split up in one test per function describe('DataView', function () {