vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

30130 lines
903 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 2.0.1-SNAPSHOT
  8. * @date 2014-07-04
  9. *
  10. * @license
  11. * Copyright (C) 2011-2014 Almende B.V, http://almende.com
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  14. * use this file except in compliance with the License. You may obtain a copy
  15. * of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  21. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  22. * License for the specific language governing permissions and limitations under
  23. * the License.
  24. */
  25. !function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.vis=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  26. /**
  27. * vis.js module imports
  28. */
  29. // Try to load dependencies from the global window object.
  30. // If not available there, load via require.
  31. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  32. var Emitter = require('emitter-component');
  33. var Hammer;
  34. if (typeof window !== 'undefined') {
  35. // load hammer.js only when running in a browser (where window is available)
  36. Hammer = window['Hammer'] || require('hammerjs');
  37. }
  38. else {
  39. Hammer = function () {
  40. throw Error('hammer.js is only available in a browser, not in node.js.');
  41. }
  42. }
  43. var mousetrap;
  44. if (typeof window !== 'undefined') {
  45. // load mousetrap.js only when running in a browser (where window is available)
  46. mousetrap = window['mousetrap'] || require('mousetrap');
  47. }
  48. else {
  49. mousetrap = function () {
  50. throw Error('mouseTrap is only available in a browser, not in node.js.');
  51. }
  52. }
  53. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  54. // it here in that case.
  55. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  56. if(!Array.prototype.indexOf) {
  57. Array.prototype.indexOf = function(obj){
  58. for(var i = 0; i < this.length; i++){
  59. if(this[i] == obj){
  60. return i;
  61. }
  62. }
  63. return -1;
  64. };
  65. try {
  66. console.log("Warning: Ancient browser detected. Please update your browser");
  67. }
  68. catch (err) {
  69. }
  70. }
  71. // Internet Explorer 8 and older does not support Array.forEach, so we define
  72. // it here in that case.
  73. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  74. if (!Array.prototype.forEach) {
  75. Array.prototype.forEach = function(fn, scope) {
  76. for(var i = 0, len = this.length; i < len; ++i) {
  77. fn.call(scope || this, this[i], i, this);
  78. }
  79. }
  80. }
  81. // Internet Explorer 8 and older does not support Array.map, so we define it
  82. // here in that case.
  83. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  84. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  85. // Reference: http://es5.github.com/#x15.4.4.19
  86. if (!Array.prototype.map) {
  87. Array.prototype.map = function(callback, thisArg) {
  88. var T, A, k;
  89. if (this == null) {
  90. throw new TypeError(" this is null or not defined");
  91. }
  92. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  93. var O = Object(this);
  94. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  95. // 3. Let len be ToUint32(lenValue).
  96. var len = O.length >>> 0;
  97. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  98. // See: http://es5.github.com/#x9.11
  99. if (typeof callback !== "function") {
  100. throw new TypeError(callback + " is not a function");
  101. }
  102. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  103. if (thisArg) {
  104. T = thisArg;
  105. }
  106. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  107. // the standard built-in constructor with that name and len is the value of len.
  108. A = new Array(len);
  109. // 7. Let k be 0
  110. k = 0;
  111. // 8. Repeat, while k < len
  112. while(k < len) {
  113. var kValue, mappedValue;
  114. // a. Let Pk be ToString(k).
  115. // This is implicit for LHS operands of the in operator
  116. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  117. // This step can be combined with c
  118. // c. If kPresent is true, then
  119. if (k in O) {
  120. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  121. kValue = O[ k ];
  122. // ii. Let mappedValue be the result of calling the Call internal method of callback
  123. // with T as the this value and argument list containing kValue, k, and O.
  124. mappedValue = callback.call(T, kValue, k, O);
  125. // iii. Call the DefineOwnProperty internal method of A with arguments
  126. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  127. // and false.
  128. // In browsers that support Object.defineProperty, use the following:
  129. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  130. // For best browser support, use the following:
  131. A[ k ] = mappedValue;
  132. }
  133. // d. Increase k by 1.
  134. k++;
  135. }
  136. // 9. return A
  137. return A;
  138. };
  139. }
  140. // Internet Explorer 8 and older does not support Array.filter, so we define it
  141. // here in that case.
  142. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  143. if (!Array.prototype.filter) {
  144. Array.prototype.filter = function(fun /*, thisp */) {
  145. "use strict";
  146. if (this == null) {
  147. throw new TypeError();
  148. }
  149. var t = Object(this);
  150. var len = t.length >>> 0;
  151. if (typeof fun != "function") {
  152. throw new TypeError();
  153. }
  154. var res = [];
  155. var thisp = arguments[1];
  156. for (var i = 0; i < len; i++) {
  157. if (i in t) {
  158. var val = t[i]; // in case fun mutates this
  159. if (fun.call(thisp, val, i, t))
  160. res.push(val);
  161. }
  162. }
  163. return res;
  164. };
  165. }
  166. // Internet Explorer 8 and older does not support Object.keys, so we define it
  167. // here in that case.
  168. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  169. if (!Object.keys) {
  170. Object.keys = (function () {
  171. var hasOwnProperty = Object.prototype.hasOwnProperty,
  172. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  173. dontEnums = [
  174. 'toString',
  175. 'toLocaleString',
  176. 'valueOf',
  177. 'hasOwnProperty',
  178. 'isPrototypeOf',
  179. 'propertyIsEnumerable',
  180. 'constructor'
  181. ],
  182. dontEnumsLength = dontEnums.length;
  183. return function (obj) {
  184. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  185. throw new TypeError('Object.keys called on non-object');
  186. }
  187. var result = [];
  188. for (var prop in obj) {
  189. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  190. }
  191. if (hasDontEnumBug) {
  192. for (var i=0; i < dontEnumsLength; i++) {
  193. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  194. }
  195. }
  196. return result;
  197. }
  198. })()
  199. }
  200. // Internet Explorer 8 and older does not support Array.isArray,
  201. // so we define it here in that case.
  202. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  203. if(!Array.isArray) {
  204. Array.isArray = function (vArg) {
  205. return Object.prototype.toString.call(vArg) === "[object Array]";
  206. };
  207. }
  208. // Internet Explorer 8 and older does not support Function.bind,
  209. // so we define it here in that case.
  210. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  211. if (!Function.prototype.bind) {
  212. Function.prototype.bind = function (oThis) {
  213. if (typeof this !== "function") {
  214. // closest thing possible to the ECMAScript 5 internal IsCallable function
  215. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  216. }
  217. var aArgs = Array.prototype.slice.call(arguments, 1),
  218. fToBind = this,
  219. fNOP = function () {},
  220. fBound = function () {
  221. return fToBind.apply(this instanceof fNOP && oThis
  222. ? this
  223. : oThis,
  224. aArgs.concat(Array.prototype.slice.call(arguments)));
  225. };
  226. fNOP.prototype = this.prototype;
  227. fBound.prototype = new fNOP();
  228. return fBound;
  229. };
  230. }
  231. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  232. if (!Object.create) {
  233. Object.create = function (o) {
  234. if (arguments.length > 1) {
  235. throw new Error('Object.create implementation only accepts the first parameter.');
  236. }
  237. function F() {}
  238. F.prototype = o;
  239. return new F();
  240. };
  241. }
  242. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  243. if (!Function.prototype.bind) {
  244. Function.prototype.bind = function (oThis) {
  245. if (typeof this !== "function") {
  246. // closest thing possible to the ECMAScript 5 internal IsCallable function
  247. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  248. }
  249. var aArgs = Array.prototype.slice.call(arguments, 1),
  250. fToBind = this,
  251. fNOP = function () {},
  252. fBound = function () {
  253. return fToBind.apply(this instanceof fNOP && oThis
  254. ? this
  255. : oThis,
  256. aArgs.concat(Array.prototype.slice.call(arguments)));
  257. };
  258. fNOP.prototype = this.prototype;
  259. fBound.prototype = new fNOP();
  260. return fBound;
  261. };
  262. }
  263. /**
  264. * utility functions
  265. */
  266. var util = {};
  267. /**
  268. * Test whether given object is a number
  269. * @param {*} object
  270. * @return {Boolean} isNumber
  271. */
  272. util.isNumber = function(object) {
  273. return (object instanceof Number || typeof object == 'number');
  274. };
  275. /**
  276. * Test whether given object is a string
  277. * @param {*} object
  278. * @return {Boolean} isString
  279. */
  280. util.isString = function(object) {
  281. return (object instanceof String || typeof object == 'string');
  282. };
  283. /**
  284. * Test whether given object is a Date, or a String containing a Date
  285. * @param {Date | String} object
  286. * @return {Boolean} isDate
  287. */
  288. util.isDate = function(object) {
  289. if (object instanceof Date) {
  290. return true;
  291. }
  292. else if (util.isString(object)) {
  293. // test whether this string contains a date
  294. var match = ASPDateRegex.exec(object);
  295. if (match) {
  296. return true;
  297. }
  298. else if (!isNaN(Date.parse(object))) {
  299. return true;
  300. }
  301. }
  302. return false;
  303. };
  304. /**
  305. * Test whether given object is an instance of google.visualization.DataTable
  306. * @param {*} object
  307. * @return {Boolean} isDataTable
  308. */
  309. util.isDataTable = function(object) {
  310. return (typeof (google) !== 'undefined') &&
  311. (google.visualization) &&
  312. (google.visualization.DataTable) &&
  313. (object instanceof google.visualization.DataTable);
  314. };
  315. /**
  316. * Create a semi UUID
  317. * source: http://stackoverflow.com/a/105074/1262753
  318. * @return {String} uuid
  319. */
  320. util.randomUUID = function() {
  321. var S4 = function () {
  322. return Math.floor(
  323. Math.random() * 0x10000 /* 65536 */
  324. ).toString(16);
  325. };
  326. return (
  327. S4() + S4() + '-' +
  328. S4() + '-' +
  329. S4() + '-' +
  330. S4() + '-' +
  331. S4() + S4() + S4()
  332. );
  333. };
  334. /**
  335. * Extend object a with the properties of object b or a series of objects
  336. * Only properties with defined values are copied
  337. * @param {Object} a
  338. * @param {... Object} b
  339. * @return {Object} a
  340. */
  341. util.extend = function (a, b) {
  342. for (var i = 1, len = arguments.length; i < len; i++) {
  343. var other = arguments[i];
  344. for (var prop in other) {
  345. if (other.hasOwnProperty(prop)) {
  346. a[prop] = other[prop];
  347. }
  348. }
  349. }
  350. return a;
  351. };
  352. /**
  353. * Extend object a with selected properties of object b or a series of objects
  354. * Only properties with defined values are copied
  355. * @param {Array.<String>} props
  356. * @param {Object} a
  357. * @param {... Object} b
  358. * @return {Object} a
  359. */
  360. util.selectiveExtend = function (props, a, b) {
  361. if (!Array.isArray(props)) {
  362. throw new Error('Array with property names expected as first argument');
  363. }
  364. for (var i = 2; i < arguments.length; i++) {
  365. var other = arguments[i];
  366. for (var p = 0; p < props.length; p++) {
  367. var prop = props[p];
  368. if (other.hasOwnProperty(prop)) {
  369. a[prop] = other[prop];
  370. }
  371. }
  372. }
  373. return a;
  374. };
  375. /**
  376. * Extend object a with selected properties of object b or a series of objects
  377. * Only properties with defined values are copied
  378. * @param {Array.<String>} props
  379. * @param {Object} a
  380. * @param {... Object} b
  381. * @return {Object} a
  382. */
  383. util.selectiveDeepExtend = function (props, a, b) {
  384. // TODO: add support for Arrays to deepExtend
  385. if (Array.isArray(b)) {
  386. throw new TypeError('Arrays are not supported by deepExtend');
  387. }
  388. for (var i = 2; i < arguments.length; i++) {
  389. var other = arguments[i];
  390. for (var p = 0; p < props.length; p++) {
  391. var prop = props[p];
  392. if (other.hasOwnProperty(prop)) {
  393. if (b[prop] && b[prop].constructor === Object) {
  394. if (a[prop] === undefined) {
  395. a[prop] = {};
  396. }
  397. if (a[prop].constructor === Object) {
  398. util.deepExtend(a[prop], b[prop]);
  399. }
  400. else {
  401. a[prop] = b[prop];
  402. }
  403. } else if (Array.isArray(b[prop])) {
  404. throw new TypeError('Arrays are not supported by deepExtend');
  405. } else {
  406. a[prop] = b[prop];
  407. }
  408. }
  409. }
  410. }
  411. return a;
  412. };
  413. /**
  414. * Deep extend an object a with the properties of object b
  415. * @param {Object} a
  416. * @param {Object} b
  417. * @returns {Object}
  418. */
  419. util.deepExtend = function(a, b) {
  420. // TODO: add support for Arrays to deepExtend
  421. if (Array.isArray(b)) {
  422. throw new TypeError('Arrays are not supported by deepExtend');
  423. }
  424. for (var prop in b) {
  425. if (b.hasOwnProperty(prop)) {
  426. if (b[prop] && b[prop].constructor === Object) {
  427. if (a[prop] === undefined) {
  428. a[prop] = {};
  429. }
  430. if (a[prop].constructor === Object) {
  431. util.deepExtend(a[prop], b[prop]);
  432. }
  433. else {
  434. a[prop] = b[prop];
  435. }
  436. } else if (Array.isArray(b[prop])) {
  437. throw new TypeError('Arrays are not supported by deepExtend');
  438. } else {
  439. a[prop] = b[prop];
  440. }
  441. }
  442. }
  443. return a;
  444. };
  445. /**
  446. * Test whether all elements in two arrays are equal.
  447. * @param {Array} a
  448. * @param {Array} b
  449. * @return {boolean} Returns true if both arrays have the same length and same
  450. * elements.
  451. */
  452. util.equalArray = function (a, b) {
  453. if (a.length != b.length) return false;
  454. for (var i = 0, len = a.length; i < len; i++) {
  455. if (a[i] != b[i]) return false;
  456. }
  457. return true;
  458. };
  459. /**
  460. * Convert an object to another type
  461. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  462. * @param {String | undefined} type Name of the type. Available types:
  463. * 'Boolean', 'Number', 'String',
  464. * 'Date', 'Moment', ISODate', 'ASPDate'.
  465. * @return {*} object
  466. * @throws Error
  467. */
  468. util.convert = function(object, type) {
  469. var match;
  470. if (object === undefined) {
  471. return undefined;
  472. }
  473. if (object === null) {
  474. return null;
  475. }
  476. if (!type) {
  477. return object;
  478. }
  479. if (!(typeof type === 'string') && !(type instanceof String)) {
  480. throw new Error('Type must be a string');
  481. }
  482. //noinspection FallthroughInSwitchStatementJS
  483. switch (type) {
  484. case 'boolean':
  485. case 'Boolean':
  486. return Boolean(object);
  487. case 'number':
  488. case 'Number':
  489. return Number(object.valueOf());
  490. case 'string':
  491. case 'String':
  492. return String(object);
  493. case 'Date':
  494. if (util.isNumber(object)) {
  495. return new Date(object);
  496. }
  497. if (object instanceof Date) {
  498. return new Date(object.valueOf());
  499. }
  500. else if (moment.isMoment(object)) {
  501. return new Date(object.valueOf());
  502. }
  503. if (util.isString(object)) {
  504. match = ASPDateRegex.exec(object);
  505. if (match) {
  506. // object is an ASP date
  507. return new Date(Number(match[1])); // parse number
  508. }
  509. else {
  510. return moment(object).toDate(); // parse string
  511. }
  512. }
  513. else {
  514. throw new Error(
  515. 'Cannot convert object of type ' + util.getType(object) +
  516. ' to type Date');
  517. }
  518. case 'Moment':
  519. if (util.isNumber(object)) {
  520. return moment(object);
  521. }
  522. if (object instanceof Date) {
  523. return moment(object.valueOf());
  524. }
  525. else if (moment.isMoment(object)) {
  526. return moment(object);
  527. }
  528. if (util.isString(object)) {
  529. match = ASPDateRegex.exec(object);
  530. if (match) {
  531. // object is an ASP date
  532. return moment(Number(match[1])); // parse number
  533. }
  534. else {
  535. return moment(object); // parse string
  536. }
  537. }
  538. else {
  539. throw new Error(
  540. 'Cannot convert object of type ' + util.getType(object) +
  541. ' to type Date');
  542. }
  543. case 'ISODate':
  544. if (util.isNumber(object)) {
  545. return new Date(object);
  546. }
  547. else if (object instanceof Date) {
  548. return object.toISOString();
  549. }
  550. else if (moment.isMoment(object)) {
  551. return object.toDate().toISOString();
  552. }
  553. else if (util.isString(object)) {
  554. match = ASPDateRegex.exec(object);
  555. if (match) {
  556. // object is an ASP date
  557. return new Date(Number(match[1])).toISOString(); // parse number
  558. }
  559. else {
  560. return new Date(object).toISOString(); // parse string
  561. }
  562. }
  563. else {
  564. throw new Error(
  565. 'Cannot convert object of type ' + util.getType(object) +
  566. ' to type ISODate');
  567. }
  568. case 'ASPDate':
  569. if (util.isNumber(object)) {
  570. return '/Date(' + object + ')/';
  571. }
  572. else if (object instanceof Date) {
  573. return '/Date(' + object.valueOf() + ')/';
  574. }
  575. else if (util.isString(object)) {
  576. match = ASPDateRegex.exec(object);
  577. var value;
  578. if (match) {
  579. // object is an ASP date
  580. value = new Date(Number(match[1])).valueOf(); // parse number
  581. }
  582. else {
  583. value = new Date(object).valueOf(); // parse string
  584. }
  585. return '/Date(' + value + ')/';
  586. }
  587. else {
  588. throw new Error(
  589. 'Cannot convert object of type ' + util.getType(object) +
  590. ' to type ASPDate');
  591. }
  592. default:
  593. throw new Error('Unknown type "' + type + '"');
  594. }
  595. };
  596. // parse ASP.Net Date pattern,
  597. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  598. // code from http://momentjs.com/
  599. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  600. /**
  601. * Get the type of an object, for example util.getType([]) returns 'Array'
  602. * @param {*} object
  603. * @return {String} type
  604. */
  605. util.getType = function(object) {
  606. var type = typeof object;
  607. if (type == 'object') {
  608. if (object == null) {
  609. return 'null';
  610. }
  611. if (object instanceof Boolean) {
  612. return 'Boolean';
  613. }
  614. if (object instanceof Number) {
  615. return 'Number';
  616. }
  617. if (object instanceof String) {
  618. return 'String';
  619. }
  620. if (object instanceof Array) {
  621. return 'Array';
  622. }
  623. if (object instanceof Date) {
  624. return 'Date';
  625. }
  626. return 'Object';
  627. }
  628. else if (type == 'number') {
  629. return 'Number';
  630. }
  631. else if (type == 'boolean') {
  632. return 'Boolean';
  633. }
  634. else if (type == 'string') {
  635. return 'String';
  636. }
  637. return type;
  638. };
  639. /**
  640. * Retrieve the absolute left value of a DOM element
  641. * @param {Element} elem A dom element, for example a div
  642. * @return {number} left The absolute left position of this element
  643. * in the browser page.
  644. */
  645. util.getAbsoluteLeft = function(elem) {
  646. var doc = document.documentElement;
  647. var body = document.body;
  648. var left = elem.offsetLeft;
  649. var e = elem.offsetParent;
  650. while (e != null && e != body && e != doc) {
  651. left += e.offsetLeft;
  652. left -= e.scrollLeft;
  653. e = e.offsetParent;
  654. }
  655. return left;
  656. };
  657. /**
  658. * Retrieve the absolute top value of a DOM element
  659. * @param {Element} elem A dom element, for example a div
  660. * @return {number} top The absolute top position of this element
  661. * in the browser page.
  662. */
  663. util.getAbsoluteTop = function(elem) {
  664. var doc = document.documentElement;
  665. var body = document.body;
  666. var top = elem.offsetTop;
  667. var e = elem.offsetParent;
  668. while (e != null && e != body && e != doc) {
  669. top += e.offsetTop;
  670. top -= e.scrollTop;
  671. e = e.offsetParent;
  672. }
  673. return top;
  674. };
  675. /**
  676. * Get the absolute, vertical mouse position from an event.
  677. * @param {Event} event
  678. * @return {Number} pageY
  679. */
  680. util.getPageY = function(event) {
  681. if ('pageY' in event) {
  682. return event.pageY;
  683. }
  684. else {
  685. var clientY;
  686. if (('targetTouches' in event) && event.targetTouches.length) {
  687. clientY = event.targetTouches[0].clientY;
  688. }
  689. else {
  690. clientY = event.clientY;
  691. }
  692. var doc = document.documentElement;
  693. var body = document.body;
  694. return clientY +
  695. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  696. ( doc && doc.clientTop || body && body.clientTop || 0 );
  697. }
  698. };
  699. /**
  700. * Get the absolute, horizontal mouse position from an event.
  701. * @param {Event} event
  702. * @return {Number} pageX
  703. */
  704. util.getPageX = function(event) {
  705. if ('pageY' in event) {
  706. return event.pageX;
  707. }
  708. else {
  709. var clientX;
  710. if (('targetTouches' in event) && event.targetTouches.length) {
  711. clientX = event.targetTouches[0].clientX;
  712. }
  713. else {
  714. clientX = event.clientX;
  715. }
  716. var doc = document.documentElement;
  717. var body = document.body;
  718. return clientX +
  719. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  720. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  721. }
  722. };
  723. /**
  724. * add a className to the given elements style
  725. * @param {Element} elem
  726. * @param {String} className
  727. */
  728. util.addClassName = function(elem, className) {
  729. var classes = elem.className.split(' ');
  730. if (classes.indexOf(className) == -1) {
  731. classes.push(className); // add the class to the array
  732. elem.className = classes.join(' ');
  733. }
  734. };
  735. /**
  736. * add a className to the given elements style
  737. * @param {Element} elem
  738. * @param {String} className
  739. */
  740. util.removeClassName = function(elem, className) {
  741. var classes = elem.className.split(' ');
  742. var index = classes.indexOf(className);
  743. if (index != -1) {
  744. classes.splice(index, 1); // remove the class from the array
  745. elem.className = classes.join(' ');
  746. }
  747. };
  748. /**
  749. * For each method for both arrays and objects.
  750. * In case of an array, the built-in Array.forEach() is applied.
  751. * In case of an Object, the method loops over all properties of the object.
  752. * @param {Object | Array} object An Object or Array
  753. * @param {function} callback Callback method, called for each item in
  754. * the object or array with three parameters:
  755. * callback(value, index, object)
  756. */
  757. util.forEach = function(object, callback) {
  758. var i,
  759. len;
  760. if (object instanceof Array) {
  761. // array
  762. for (i = 0, len = object.length; i < len; i++) {
  763. callback(object[i], i, object);
  764. }
  765. }
  766. else {
  767. // object
  768. for (i in object) {
  769. if (object.hasOwnProperty(i)) {
  770. callback(object[i], i, object);
  771. }
  772. }
  773. }
  774. };
  775. /**
  776. * Convert an object into an array: all objects properties are put into the
  777. * array. The resulting array is unordered.
  778. * @param {Object} object
  779. * @param {Array} array
  780. */
  781. util.toArray = function(object) {
  782. var array = [];
  783. for (var prop in object) {
  784. if (object.hasOwnProperty(prop)) array.push(object[prop]);
  785. }
  786. return array;
  787. }
  788. /**
  789. * Update a property in an object
  790. * @param {Object} object
  791. * @param {String} key
  792. * @param {*} value
  793. * @return {Boolean} changed
  794. */
  795. util.updateProperty = function(object, key, value) {
  796. if (object[key] !== value) {
  797. object[key] = value;
  798. return true;
  799. }
  800. else {
  801. return false;
  802. }
  803. };
  804. /**
  805. * Add and event listener. Works for all browsers
  806. * @param {Element} element An html element
  807. * @param {string} action The action, for example "click",
  808. * without the prefix "on"
  809. * @param {function} listener The callback function to be executed
  810. * @param {boolean} [useCapture]
  811. */
  812. util.addEventListener = function(element, action, listener, useCapture) {
  813. if (element.addEventListener) {
  814. if (useCapture === undefined)
  815. useCapture = false;
  816. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  817. action = "DOMMouseScroll"; // For Firefox
  818. }
  819. element.addEventListener(action, listener, useCapture);
  820. } else {
  821. element.attachEvent("on" + action, listener); // IE browsers
  822. }
  823. };
  824. /**
  825. * Remove an event listener from an element
  826. * @param {Element} element An html dom element
  827. * @param {string} action The name of the event, for example "mousedown"
  828. * @param {function} listener The listener function
  829. * @param {boolean} [useCapture]
  830. */
  831. util.removeEventListener = function(element, action, listener, useCapture) {
  832. if (element.removeEventListener) {
  833. // non-IE browsers
  834. if (useCapture === undefined)
  835. useCapture = false;
  836. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  837. action = "DOMMouseScroll"; // For Firefox
  838. }
  839. element.removeEventListener(action, listener, useCapture);
  840. } else {
  841. // IE browsers
  842. element.detachEvent("on" + action, listener);
  843. }
  844. };
  845. /**
  846. * Get HTML element which is the target of the event
  847. * @param {Event} event
  848. * @return {Element} target element
  849. */
  850. util.getTarget = function(event) {
  851. // code from http://www.quirksmode.org/js/events_properties.html
  852. if (!event) {
  853. event = window.event;
  854. }
  855. var target;
  856. if (event.target) {
  857. target = event.target;
  858. }
  859. else if (event.srcElement) {
  860. target = event.srcElement;
  861. }
  862. if (target.nodeType != undefined && target.nodeType == 3) {
  863. // defeat Safari bug
  864. target = target.parentNode;
  865. }
  866. return target;
  867. };
  868. /**
  869. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  870. * @param {Element} element
  871. * @param {Event} event
  872. */
  873. util.fakeGesture = function(element, event) {
  874. var eventType = null;
  875. // for hammer.js 1.0.5
  876. var gesture = Hammer.event.collectEventData(this, eventType, event);
  877. // for hammer.js 1.0.6
  878. //var touches = Hammer.event.getTouchList(event, eventType);
  879. // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
  880. // on IE in standards mode, no touches are recognized by hammer.js,
  881. // resulting in NaN values for center.pageX and center.pageY
  882. if (isNaN(gesture.center.pageX)) {
  883. gesture.center.pageX = event.pageX;
  884. }
  885. if (isNaN(gesture.center.pageY)) {
  886. gesture.center.pageY = event.pageY;
  887. }
  888. return gesture;
  889. };
  890. util.option = {};
  891. /**
  892. * Convert a value into a boolean
  893. * @param {Boolean | function | undefined} value
  894. * @param {Boolean} [defaultValue]
  895. * @returns {Boolean} bool
  896. */
  897. util.option.asBoolean = function (value, defaultValue) {
  898. if (typeof value == 'function') {
  899. value = value();
  900. }
  901. if (value != null) {
  902. return (value != false);
  903. }
  904. return defaultValue || null;
  905. };
  906. /**
  907. * Convert a value into a number
  908. * @param {Boolean | function | undefined} value
  909. * @param {Number} [defaultValue]
  910. * @returns {Number} number
  911. */
  912. util.option.asNumber = function (value, defaultValue) {
  913. if (typeof value == 'function') {
  914. value = value();
  915. }
  916. if (value != null) {
  917. return Number(value) || defaultValue || null;
  918. }
  919. return defaultValue || null;
  920. };
  921. /**
  922. * Convert a value into a string
  923. * @param {String | function | undefined} value
  924. * @param {String} [defaultValue]
  925. * @returns {String} str
  926. */
  927. util.option.asString = function (value, defaultValue) {
  928. if (typeof value == 'function') {
  929. value = value();
  930. }
  931. if (value != null) {
  932. return String(value);
  933. }
  934. return defaultValue || null;
  935. };
  936. /**
  937. * Convert a size or location into a string with pixels or a percentage
  938. * @param {String | Number | function | undefined} value
  939. * @param {String} [defaultValue]
  940. * @returns {String} size
  941. */
  942. util.option.asSize = function (value, defaultValue) {
  943. if (typeof value == 'function') {
  944. value = value();
  945. }
  946. if (util.isString(value)) {
  947. return value;
  948. }
  949. else if (util.isNumber(value)) {
  950. return value + 'px';
  951. }
  952. else {
  953. return defaultValue || null;
  954. }
  955. };
  956. /**
  957. * Convert a value into a DOM element
  958. * @param {HTMLElement | function | undefined} value
  959. * @param {HTMLElement} [defaultValue]
  960. * @returns {HTMLElement | null} dom
  961. */
  962. util.option.asElement = function (value, defaultValue) {
  963. if (typeof value == 'function') {
  964. value = value();
  965. }
  966. return value || defaultValue || null;
  967. };
  968. util.GiveDec = function(Hex) {
  969. var Value;
  970. if (Hex == "A")
  971. Value = 10;
  972. else if (Hex == "B")
  973. Value = 11;
  974. else if (Hex == "C")
  975. Value = 12;
  976. else if (Hex == "D")
  977. Value = 13;
  978. else if (Hex == "E")
  979. Value = 14;
  980. else if (Hex == "F")
  981. Value = 15;
  982. else
  983. Value = eval(Hex);
  984. return Value;
  985. };
  986. util.GiveHex = function(Dec) {
  987. var Value;
  988. if(Dec == 10)
  989. Value = "A";
  990. else if (Dec == 11)
  991. Value = "B";
  992. else if (Dec == 12)
  993. Value = "C";
  994. else if (Dec == 13)
  995. Value = "D";
  996. else if (Dec == 14)
  997. Value = "E";
  998. else if (Dec == 15)
  999. Value = "F";
  1000. else
  1001. Value = "" + Dec;
  1002. return Value;
  1003. };
  1004. /**
  1005. * Parse a color property into an object with border, background, and
  1006. * highlight colors
  1007. * @param {Object | String} color
  1008. * @return {Object} colorObject
  1009. */
  1010. util.parseColor = function(color) {
  1011. var c;
  1012. if (util.isString(color)) {
  1013. if (util.isValidHex(color)) {
  1014. var hsv = util.hexToHSV(color);
  1015. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  1016. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  1017. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  1018. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  1019. c = {
  1020. background: color,
  1021. border:darkerColorHex,
  1022. highlight: {
  1023. background:lighterColorHex,
  1024. border:darkerColorHex
  1025. },
  1026. hover: {
  1027. background:lighterColorHex,
  1028. border:darkerColorHex
  1029. }
  1030. };
  1031. }
  1032. else {
  1033. c = {
  1034. background:color,
  1035. border:color,
  1036. highlight: {
  1037. background:color,
  1038. border:color
  1039. },
  1040. hover: {
  1041. background:color,
  1042. border:color
  1043. }
  1044. };
  1045. }
  1046. }
  1047. else {
  1048. c = {};
  1049. c.background = color.background || 'white';
  1050. c.border = color.border || c.background;
  1051. if (util.isString(color.highlight)) {
  1052. c.highlight = {
  1053. border: color.highlight,
  1054. background: color.highlight
  1055. }
  1056. }
  1057. else {
  1058. c.highlight = {};
  1059. c.highlight.background = color.highlight && color.highlight.background || c.background;
  1060. c.highlight.border = color.highlight && color.highlight.border || c.border;
  1061. }
  1062. if (util.isString(color.hover)) {
  1063. c.hover = {
  1064. border: color.hover,
  1065. background: color.hover
  1066. }
  1067. }
  1068. else {
  1069. c.hover = {};
  1070. c.hover.background = color.hover && color.hover.background || c.background;
  1071. c.hover.border = color.hover && color.hover.border || c.border;
  1072. }
  1073. }
  1074. return c;
  1075. };
  1076. /**
  1077. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  1078. *
  1079. * @param {String} hex
  1080. * @returns {{r: *, g: *, b: *}}
  1081. */
  1082. util.hexToRGB = function(hex) {
  1083. hex = hex.replace("#","").toUpperCase();
  1084. var a = util.GiveDec(hex.substring(0, 1));
  1085. var b = util.GiveDec(hex.substring(1, 2));
  1086. var c = util.GiveDec(hex.substring(2, 3));
  1087. var d = util.GiveDec(hex.substring(3, 4));
  1088. var e = util.GiveDec(hex.substring(4, 5));
  1089. var f = util.GiveDec(hex.substring(5, 6));
  1090. var r = (a * 16) + b;
  1091. var g = (c * 16) + d;
  1092. var b = (e * 16) + f;
  1093. return {r:r,g:g,b:b};
  1094. };
  1095. util.RGBToHex = function(red,green,blue) {
  1096. var a = util.GiveHex(Math.floor(red / 16));
  1097. var b = util.GiveHex(red % 16);
  1098. var c = util.GiveHex(Math.floor(green / 16));
  1099. var d = util.GiveHex(green % 16);
  1100. var e = util.GiveHex(Math.floor(blue / 16));
  1101. var f = util.GiveHex(blue % 16);
  1102. var hex = a + b + c + d + e + f;
  1103. return "#" + hex;
  1104. };
  1105. /**
  1106. * http://www.javascripter.net/faq/rgb2hsv.htm
  1107. *
  1108. * @param red
  1109. * @param green
  1110. * @param blue
  1111. * @returns {*}
  1112. * @constructor
  1113. */
  1114. util.RGBToHSV = function(red,green,blue) {
  1115. red=red/255; green=green/255; blue=blue/255;
  1116. var minRGB = Math.min(red,Math.min(green,blue));
  1117. var maxRGB = Math.max(red,Math.max(green,blue));
  1118. // Black-gray-white
  1119. if (minRGB == maxRGB) {
  1120. return {h:0,s:0,v:minRGB};
  1121. }
  1122. // Colors other than black-gray-white:
  1123. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  1124. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  1125. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  1126. var saturation = (maxRGB - minRGB)/maxRGB;
  1127. var value = maxRGB;
  1128. return {h:hue,s:saturation,v:value};
  1129. };
  1130. /**
  1131. * https://gist.github.com/mjijackson/5311256
  1132. * @param hue
  1133. * @param saturation
  1134. * @param value
  1135. * @returns {{r: number, g: number, b: number}}
  1136. * @constructor
  1137. */
  1138. util.HSVToRGB = function(h, s, v) {
  1139. var r, g, b;
  1140. var i = Math.floor(h * 6);
  1141. var f = h * 6 - i;
  1142. var p = v * (1 - s);
  1143. var q = v * (1 - f * s);
  1144. var t = v * (1 - (1 - f) * s);
  1145. switch (i % 6) {
  1146. case 0: r = v, g = t, b = p; break;
  1147. case 1: r = q, g = v, b = p; break;
  1148. case 2: r = p, g = v, b = t; break;
  1149. case 3: r = p, g = q, b = v; break;
  1150. case 4: r = t, g = p, b = v; break;
  1151. case 5: r = v, g = p, b = q; break;
  1152. }
  1153. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  1154. };
  1155. util.HSVToHex = function(h, s, v) {
  1156. var rgb = util.HSVToRGB(h, s, v);
  1157. return util.RGBToHex(rgb.r, rgb.g, rgb.b);
  1158. };
  1159. util.hexToHSV = function(hex) {
  1160. var rgb = util.hexToRGB(hex);
  1161. return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1162. };
  1163. util.isValidHex = function(hex) {
  1164. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1165. return isOk;
  1166. };
  1167. /**
  1168. * This recursively redirects the prototype of JSON objects to the referenceObject
  1169. * This is used for default options.
  1170. *
  1171. * @param referenceObject
  1172. * @returns {*}
  1173. */
  1174. util.selectiveBridgeObject = function(fields, referenceObject) {
  1175. if (typeof referenceObject == "object") {
  1176. var objectTo = Object.create(referenceObject);
  1177. for (var i = 0; i < fields.length; i++) {
  1178. if (referenceObject.hasOwnProperty(fields[i])) {
  1179. if (typeof referenceObject[fields[i]] == "object") {
  1180. objectTo[fields[i]] = util.bridgeObject(referenceObject[fields[i]]);
  1181. }
  1182. }
  1183. }
  1184. return objectTo;
  1185. }
  1186. else {
  1187. return null;
  1188. }
  1189. };
  1190. /**
  1191. * This recursively redirects the prototype of JSON objects to the referenceObject
  1192. * This is used for default options.
  1193. *
  1194. * @param referenceObject
  1195. * @returns {*}
  1196. */
  1197. util.bridgeObject = function(referenceObject) {
  1198. if (typeof referenceObject == "object") {
  1199. var objectTo = Object.create(referenceObject);
  1200. for (var i in referenceObject) {
  1201. if (referenceObject.hasOwnProperty(i)) {
  1202. if (typeof referenceObject[i] == "object") {
  1203. objectTo[i] = util.bridgeObject(referenceObject[i]);
  1204. }
  1205. }
  1206. }
  1207. return objectTo;
  1208. }
  1209. else {
  1210. return null;
  1211. }
  1212. };
  1213. /**
  1214. * this is used to set the options of subobjects in the options object. A requirement of these subobjects
  1215. * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
  1216. *
  1217. * @param [object] mergeTarget | this is either this.options or the options used for the groups.
  1218. * @param [object] options | options
  1219. * @param [String] option | this is the option key in the options argument
  1220. * @private
  1221. */
  1222. util.mergeOptions = function (mergeTarget, options, option) {
  1223. if (options[option] !== undefined) {
  1224. if (typeof options[option] == 'boolean') {
  1225. mergeTarget[option].enabled = options[option];
  1226. }
  1227. else {
  1228. mergeTarget[option].enabled = true;
  1229. for (prop in options[option]) {
  1230. if (options[option].hasOwnProperty(prop)) {
  1231. mergeTarget[option][prop] = options[option][prop];
  1232. }
  1233. }
  1234. }
  1235. }
  1236. }
  1237. /**
  1238. * this is used to set the options of subobjects in the options object. A requirement of these subobjects
  1239. * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
  1240. *
  1241. * @param [object] mergeTarget | this is either this.options or the options used for the groups.
  1242. * @param [object] options | options
  1243. * @param [String] option | this is the option key in the options argument
  1244. * @private
  1245. */
  1246. util.mergeOptions = function (mergeTarget, options, option) {
  1247. if (options[option] !== undefined) {
  1248. if (typeof options[option] == 'boolean') {
  1249. mergeTarget[option].enabled = options[option];
  1250. }
  1251. else {
  1252. mergeTarget[option].enabled = true;
  1253. for (prop in options[option]) {
  1254. if (options[option].hasOwnProperty(prop)) {
  1255. mergeTarget[option][prop] = options[option][prop];
  1256. }
  1257. }
  1258. }
  1259. }
  1260. }
  1261. /**
  1262. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  1263. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  1264. * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
  1265. * if the time we selected (start or end) is within the current range).
  1266. *
  1267. * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
  1268. * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
  1269. * either the start OR end time has to be in the range.
  1270. *
  1271. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems
  1272. * @param {{start: number, end: number}} range
  1273. * @param {Boolean} byEnd
  1274. * @returns {number}
  1275. * @private
  1276. */
  1277. util.binarySearch = function(orderedItems, range, field, field2) {
  1278. var array = orderedItems;
  1279. var interval = range.end - range.start;
  1280. var found = false;
  1281. var low = 0;
  1282. var high = array.length;
  1283. var guess = Math.floor(0.5*(high+low));
  1284. var newGuess;
  1285. var value;
  1286. if (high == 0) {guess = -1;}
  1287. else if (high == 1) {
  1288. value = field2 === undefined ? array[guess][field] : array[guess][field][field2];
  1289. if ((value > range.start - interval) && (value < range.end)) {
  1290. guess = 0;
  1291. }
  1292. else {
  1293. guess = -1;
  1294. }
  1295. }
  1296. else {
  1297. high -= 1;
  1298. while (found == false) {
  1299. value = field2 === undefined ? array[guess][field] : array[guess][field][field2];
  1300. if ((value > range.start - interval) && (value < range.end)) {
  1301. found = true;
  1302. }
  1303. else {
  1304. if (value < range.start - interval) { // it is too small --> increase low
  1305. low = Math.floor(0.5*(high+low));
  1306. }
  1307. else { // it is too big --> decrease high
  1308. high = Math.floor(0.5*(high+low));
  1309. }
  1310. newGuess = Math.floor(0.5*(high+low));
  1311. // not in list;
  1312. if (guess == newGuess) {
  1313. guess = -1;
  1314. found = true;
  1315. }
  1316. else {
  1317. guess = newGuess;
  1318. }
  1319. }
  1320. }
  1321. }
  1322. return guess;
  1323. };
  1324. /**
  1325. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  1326. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  1327. * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
  1328. * if the time we selected (start or end) is within the current range).
  1329. *
  1330. * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
  1331. * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
  1332. * either the start OR end time has to be in the range.
  1333. *
  1334. * @param {Array} orderedItems
  1335. * @param {{start: number, end: number}} target
  1336. * @param {Boolean} byEnd
  1337. * @returns {number}
  1338. * @private
  1339. */
  1340. util.binarySearchGeneric = function(orderedItems, target, field, sidePreference) {
  1341. var array = orderedItems;
  1342. var found = false;
  1343. var low = 0;
  1344. var high = array.length;
  1345. var guess = Math.floor(0.5*(high+low));
  1346. var newGuess;
  1347. var prevValue, value, nextValue;
  1348. if (high == 0) {guess = -1;}
  1349. else if (high == 1) {
  1350. value = array[guess][field];
  1351. if (value == target) {
  1352. guess = 0;
  1353. }
  1354. else {
  1355. guess = -1;
  1356. }
  1357. }
  1358. else {
  1359. high -= 1;
  1360. while (found == false) {
  1361. prevValue = array[Math.max(0,guess - 1)][field];
  1362. value = array[guess][field];
  1363. nextValue = array[Math.min(array.length-1,guess + 1)][field];
  1364. if (value == target || prevValue < target && value > target || value < target && nextValue > target) {
  1365. found = true;
  1366. if (value != target) {
  1367. if (sidePreference == 'before') {
  1368. if (prevValue < target && value > target) {
  1369. guess = Math.max(0,guess - 1);
  1370. }
  1371. }
  1372. else {
  1373. if (value < target && nextValue > target) {
  1374. guess = Math.min(array.length-1,guess + 1);
  1375. }
  1376. }
  1377. }
  1378. }
  1379. else {
  1380. if (value < target) { // it is too small --> increase low
  1381. low = Math.floor(0.5*(high+low));
  1382. }
  1383. else { // it is too big --> decrease high
  1384. high = Math.floor(0.5*(high+low));
  1385. }
  1386. newGuess = Math.floor(0.5*(high+low));
  1387. // not in list;
  1388. if (guess == newGuess) {
  1389. guess = -2;
  1390. found = true;
  1391. }
  1392. else {
  1393. guess = newGuess;
  1394. }
  1395. }
  1396. }
  1397. }
  1398. return guess;
  1399. };
  1400. /**
  1401. * Created by Alex on 6/20/14.
  1402. */
  1403. var DOMutil = {}
  1404. /**
  1405. * this prepares the JSON container for allocating SVG elements
  1406. * @param JSONcontainer
  1407. * @private
  1408. */
  1409. DOMutil.prepareElements = function(JSONcontainer) {
  1410. // cleanup the redundant svgElements;
  1411. for (var elementType in JSONcontainer) {
  1412. if (JSONcontainer.hasOwnProperty(elementType)) {
  1413. JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
  1414. JSONcontainer[elementType].used = [];
  1415. }
  1416. }
  1417. };
  1418. /**
  1419. * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
  1420. * which to remove the redundant elements.
  1421. *
  1422. * @param JSONcontainer
  1423. * @private
  1424. */
  1425. DOMutil.cleanupElements = function(JSONcontainer) {
  1426. // cleanup the redundant svgElements;
  1427. for (var elementType in JSONcontainer) {
  1428. if (JSONcontainer.hasOwnProperty(elementType)) {
  1429. if (JSONcontainer[elementType].redundant) {
  1430. for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
  1431. JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
  1432. }
  1433. JSONcontainer[elementType].redundant = [];
  1434. }
  1435. }
  1436. }
  1437. };
  1438. /**
  1439. * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
  1440. * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
  1441. *
  1442. * @param elementType
  1443. * @param JSONcontainer
  1444. * @param svgContainer
  1445. * @returns {*}
  1446. * @private
  1447. */
  1448. DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
  1449. var element;
  1450. // allocate SVG element, if it doesnt yet exist, create one.
  1451. if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
  1452. // check if there is an redundant element
  1453. if (JSONcontainer[elementType].redundant.length > 0) {
  1454. element = JSONcontainer[elementType].redundant[0];
  1455. JSONcontainer[elementType].redundant.shift();
  1456. }
  1457. else {
  1458. // create a new element and add it to the SVG
  1459. element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
  1460. svgContainer.appendChild(element);
  1461. }
  1462. }
  1463. else {
  1464. // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
  1465. element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
  1466. JSONcontainer[elementType] = {used: [], redundant: []};
  1467. svgContainer.appendChild(element);
  1468. }
  1469. JSONcontainer[elementType].used.push(element);
  1470. return element;
  1471. };
  1472. /**
  1473. * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
  1474. * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
  1475. *
  1476. * @param elementType
  1477. * @param JSONcontainer
  1478. * @param DOMContainer
  1479. * @returns {*}
  1480. * @private
  1481. */
  1482. DOMutil.getDOMElement = function (elementType, JSONcontainer, DOMContainer) {
  1483. var element;
  1484. // allocate SVG element, if it doesnt yet exist, create one.
  1485. if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
  1486. // check if there is an redundant element
  1487. if (JSONcontainer[elementType].redundant.length > 0) {
  1488. element = JSONcontainer[elementType].redundant[0];
  1489. JSONcontainer[elementType].redundant.shift();
  1490. }
  1491. else {
  1492. // create a new element and add it to the SVG
  1493. element = document.createElement(elementType);
  1494. DOMContainer.appendChild(element);
  1495. }
  1496. }
  1497. else {
  1498. // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
  1499. element = document.createElement(elementType);
  1500. JSONcontainer[elementType] = {used: [], redundant: []};
  1501. DOMContainer.appendChild(element);
  1502. }
  1503. JSONcontainer[elementType].used.push(element);
  1504. return element;
  1505. };
  1506. /**
  1507. * draw a point object. this is a seperate function because it can also be called by the legend.
  1508. * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
  1509. * as well.
  1510. *
  1511. * @param x
  1512. * @param y
  1513. * @param group
  1514. * @param JSONcontainer
  1515. * @param svgContainer
  1516. * @returns {*}
  1517. */
  1518. DOMutil.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
  1519. var point;
  1520. if (group.options.drawPoints.style == 'circle') {
  1521. point = DOMutil.getSVGElement('circle',JSONcontainer,svgContainer);
  1522. point.setAttributeNS(null, "cx", x);
  1523. point.setAttributeNS(null, "cy", y);
  1524. point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size);
  1525. point.setAttributeNS(null, "class", group.className + " point");
  1526. }
  1527. else {
  1528. point = DOMutil.getSVGElement('rect',JSONcontainer,svgContainer);
  1529. point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size);
  1530. point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size);
  1531. point.setAttributeNS(null, "width", group.options.drawPoints.size);
  1532. point.setAttributeNS(null, "height", group.options.drawPoints.size);
  1533. point.setAttributeNS(null, "class", group.className + " point");
  1534. }
  1535. return point;
  1536. };
  1537. /**
  1538. * draw a bar SVG element centered on the X coordinate
  1539. *
  1540. * @param x
  1541. * @param y
  1542. * @param className
  1543. */
  1544. DOMutil.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
  1545. rect = DOMutil.getSVGElement('rect',JSONcontainer, svgContainer);
  1546. rect.setAttributeNS(null, "x", x - 0.5 * width);
  1547. rect.setAttributeNS(null, "y", y);
  1548. rect.setAttributeNS(null, "width", width);
  1549. rect.setAttributeNS(null, "height", height);
  1550. rect.setAttributeNS(null, "class", className);
  1551. };
  1552. /**
  1553. * DataSet
  1554. *
  1555. * Usage:
  1556. * var dataSet = new DataSet({
  1557. * fieldId: '_id',
  1558. * type: {
  1559. * // ...
  1560. * }
  1561. * });
  1562. *
  1563. * dataSet.add(item);
  1564. * dataSet.add(data);
  1565. * dataSet.update(item);
  1566. * dataSet.update(data);
  1567. * dataSet.remove(id);
  1568. * dataSet.remove(ids);
  1569. * var data = dataSet.get();
  1570. * var data = dataSet.get(id);
  1571. * var data = dataSet.get(ids);
  1572. * var data = dataSet.get(ids, options, data);
  1573. * dataSet.clear();
  1574. *
  1575. * A data set can:
  1576. * - add/remove/update data
  1577. * - gives triggers upon changes in the data
  1578. * - can import/export data in various data formats
  1579. *
  1580. * @param {Array | DataTable} [data] Optional array with initial data
  1581. * @param {Object} [options] Available options:
  1582. * {String} fieldId Field name of the id in the
  1583. * items, 'id' by default.
  1584. * {Object.<String, String} type
  1585. * A map with field names as key,
  1586. * and the field type as value.
  1587. * @constructor DataSet
  1588. */
  1589. // TODO: add a DataSet constructor DataSet(data, options)
  1590. function DataSet (data, options) {
  1591. // correctly read optional arguments
  1592. if (data && !Array.isArray(data) && !util.isDataTable(data)) {
  1593. options = data;
  1594. data = null;
  1595. }
  1596. this._options = options || {};
  1597. this._data = {}; // map with data indexed by id
  1598. this._fieldId = this._options.fieldId || 'id'; // name of the field containing id
  1599. this._type = {}; // internal field types (NOTE: this can differ from this._options.type)
  1600. // all variants of a Date are internally stored as Date, so we can convert
  1601. // from everything to everything (also from ISODate to Number for example)
  1602. if (this._options.type) {
  1603. for (var field in this._options.type) {
  1604. if (this._options.type.hasOwnProperty(field)) {
  1605. var value = this._options.type[field];
  1606. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1607. this._type[field] = 'Date';
  1608. }
  1609. else {
  1610. this._type[field] = value;
  1611. }
  1612. }
  1613. }
  1614. }
  1615. // TODO: deprecated since version 1.1.1 (or 2.0.0?)
  1616. if (this._options.convert) {
  1617. throw new Error('Option "convert" is deprecated. Use "type" instead.');
  1618. }
  1619. this._subscribers = {}; // event subscribers
  1620. // add initial data when provided
  1621. if (data) {
  1622. this.add(data);
  1623. }
  1624. }
  1625. /**
  1626. * Subscribe to an event, add an event listener
  1627. * @param {String} event Event name. Available events: 'put', 'update',
  1628. * 'remove'
  1629. * @param {function} callback Callback method. Called with three parameters:
  1630. * {String} event
  1631. * {Object | null} params
  1632. * {String | Number} senderId
  1633. */
  1634. DataSet.prototype.on = function(event, callback) {
  1635. var subscribers = this._subscribers[event];
  1636. if (!subscribers) {
  1637. subscribers = [];
  1638. this._subscribers[event] = subscribers;
  1639. }
  1640. subscribers.push({
  1641. callback: callback
  1642. });
  1643. };
  1644. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1645. DataSet.prototype.subscribe = DataSet.prototype.on;
  1646. /**
  1647. * Unsubscribe from an event, remove an event listener
  1648. * @param {String} event
  1649. * @param {function} callback
  1650. */
  1651. DataSet.prototype.off = function(event, callback) {
  1652. var subscribers = this._subscribers[event];
  1653. if (subscribers) {
  1654. this._subscribers[event] = subscribers.filter(function (listener) {
  1655. return (listener.callback != callback);
  1656. });
  1657. }
  1658. };
  1659. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1660. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1661. /**
  1662. * Trigger an event
  1663. * @param {String} event
  1664. * @param {Object | null} params
  1665. * @param {String} [senderId] Optional id of the sender.
  1666. * @private
  1667. */
  1668. DataSet.prototype._trigger = function (event, params, senderId) {
  1669. if (event == '*') {
  1670. throw new Error('Cannot trigger event *');
  1671. }
  1672. var subscribers = [];
  1673. if (event in this._subscribers) {
  1674. subscribers = subscribers.concat(this._subscribers[event]);
  1675. }
  1676. if ('*' in this._subscribers) {
  1677. subscribers = subscribers.concat(this._subscribers['*']);
  1678. }
  1679. for (var i = 0; i < subscribers.length; i++) {
  1680. var subscriber = subscribers[i];
  1681. if (subscriber.callback) {
  1682. subscriber.callback(event, params, senderId || null);
  1683. }
  1684. }
  1685. };
  1686. /**
  1687. * Add data.
  1688. * Adding an item will fail when there already is an item with the same id.
  1689. * @param {Object | Array | DataTable} data
  1690. * @param {String} [senderId] Optional sender id
  1691. * @return {Array} addedIds Array with the ids of the added items
  1692. */
  1693. DataSet.prototype.add = function (data, senderId) {
  1694. var addedIds = [],
  1695. id,
  1696. me = this;
  1697. if (Array.isArray(data)) {
  1698. // Array
  1699. for (var i = 0, len = data.length; i < len; i++) {
  1700. id = me._addItem(data[i]);
  1701. addedIds.push(id);
  1702. }
  1703. }
  1704. else if (util.isDataTable(data)) {
  1705. // Google DataTable
  1706. var columns = this._getColumnNames(data);
  1707. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1708. var item = {};
  1709. for (var col = 0, cols = columns.length; col < cols; col++) {
  1710. var field = columns[col];
  1711. item[field] = data.getValue(row, col);
  1712. }
  1713. id = me._addItem(item);
  1714. addedIds.push(id);
  1715. }
  1716. }
  1717. else if (data instanceof Object) {
  1718. // Single item
  1719. id = me._addItem(data);
  1720. addedIds.push(id);
  1721. }
  1722. else {
  1723. throw new Error('Unknown dataType');
  1724. }
  1725. if (addedIds.length) {
  1726. this._trigger('add', {items: addedIds}, senderId);
  1727. }
  1728. return addedIds;
  1729. };
  1730. /**
  1731. * Update existing items. When an item does not exist, it will be created
  1732. * @param {Object | Array | DataTable} data
  1733. * @param {String} [senderId] Optional sender id
  1734. * @return {Array} updatedIds The ids of the added or updated items
  1735. */
  1736. DataSet.prototype.update = function (data, senderId) {
  1737. var addedIds = [],
  1738. updatedIds = [],
  1739. me = this,
  1740. fieldId = me._fieldId;
  1741. var addOrUpdate = function (item) {
  1742. var id = item[fieldId];
  1743. if (me._data[id]) {
  1744. // update item
  1745. id = me._updateItem(item);
  1746. updatedIds.push(id);
  1747. }
  1748. else {
  1749. // add new item
  1750. id = me._addItem(item);
  1751. addedIds.push(id);
  1752. }
  1753. };
  1754. if (Array.isArray(data)) {
  1755. // Array
  1756. for (var i = 0, len = data.length; i < len; i++) {
  1757. addOrUpdate(data[i]);
  1758. }
  1759. }
  1760. else if (util.isDataTable(data)) {
  1761. // Google DataTable
  1762. var columns = this._getColumnNames(data);
  1763. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1764. var item = {};
  1765. for (var col = 0, cols = columns.length; col < cols; col++) {
  1766. var field = columns[col];
  1767. item[field] = data.getValue(row, col);
  1768. }
  1769. addOrUpdate(item);
  1770. }
  1771. }
  1772. else if (data instanceof Object) {
  1773. // Single item
  1774. addOrUpdate(data);
  1775. }
  1776. else {
  1777. throw new Error('Unknown dataType');
  1778. }
  1779. if (addedIds.length) {
  1780. this._trigger('add', {items: addedIds}, senderId);
  1781. }
  1782. if (updatedIds.length) {
  1783. this._trigger('update', {items: updatedIds}, senderId);
  1784. }
  1785. return addedIds.concat(updatedIds);
  1786. };
  1787. /**
  1788. * Get a data item or multiple items.
  1789. *
  1790. * Usage:
  1791. *
  1792. * get()
  1793. * get(options: Object)
  1794. * get(options: Object, data: Array | DataTable)
  1795. *
  1796. * get(id: Number | String)
  1797. * get(id: Number | String, options: Object)
  1798. * get(id: Number | String, options: Object, data: Array | DataTable)
  1799. *
  1800. * get(ids: Number[] | String[])
  1801. * get(ids: Number[] | String[], options: Object)
  1802. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1803. *
  1804. * Where:
  1805. *
  1806. * {Number | String} id The id of an item
  1807. * {Number[] | String{}} ids An array with ids of items
  1808. * {Object} options An Object with options. Available options:
  1809. * {String} [returnType] Type of data to be
  1810. * returned. Can be 'DataTable' or 'Array' (default)
  1811. * {Object.<String, String>} [type]
  1812. * {String[]} [fields] field names to be returned
  1813. * {function} [filter] filter items
  1814. * {String | function} [order] Order the items by
  1815. * a field name or custom sort function.
  1816. * {Array | DataTable} [data] If provided, items will be appended to this
  1817. * array or table. Required in case of Google
  1818. * DataTable.
  1819. *
  1820. * @throws Error
  1821. */
  1822. DataSet.prototype.get = function (args) {
  1823. var me = this;
  1824. // parse the arguments
  1825. var id, ids, options, data;
  1826. var firstType = util.getType(arguments[0]);
  1827. if (firstType == 'String' || firstType == 'Number') {
  1828. // get(id [, options] [, data])
  1829. id = arguments[0];
  1830. options = arguments[1];
  1831. data = arguments[2];
  1832. }
  1833. else if (firstType == 'Array') {
  1834. // get(ids [, options] [, data])
  1835. ids = arguments[0];
  1836. options = arguments[1];
  1837. data = arguments[2];
  1838. }
  1839. else {
  1840. // get([, options] [, data])
  1841. options = arguments[0];
  1842. data = arguments[1];
  1843. }
  1844. // determine the return type
  1845. var returnType;
  1846. if (options && options.returnType) {
  1847. returnType = (options.returnType == 'DataTable') ? 'DataTable' : 'Array';
  1848. if (data && (returnType != util.getType(data))) {
  1849. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1850. 'does not correspond with specified options.type (' + options.type + ')');
  1851. }
  1852. if (returnType == 'DataTable' && !util.isDataTable(data)) {
  1853. throw new Error('Parameter "data" must be a DataTable ' +
  1854. 'when options.type is "DataTable"');
  1855. }
  1856. }
  1857. else if (data) {
  1858. returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1859. }
  1860. else {
  1861. returnType = 'Array';
  1862. }
  1863. // build options
  1864. var type = options && options.type || this._options.type;
  1865. var filter = options && options.filter;
  1866. var items = [], item, itemId, i, len;
  1867. // convert items
  1868. if (id != undefined) {
  1869. // return a single item
  1870. item = me._getItem(id, type);
  1871. if (filter && !filter(item)) {
  1872. item = null;
  1873. }
  1874. }
  1875. else if (ids != undefined) {
  1876. // return a subset of items
  1877. for (i = 0, len = ids.length; i < len; i++) {
  1878. item = me._getItem(ids[i], type);
  1879. if (!filter || filter(item)) {
  1880. items.push(item);
  1881. }
  1882. }
  1883. }
  1884. else {
  1885. // return all items
  1886. for (itemId in this._data) {
  1887. if (this._data.hasOwnProperty(itemId)) {
  1888. item = me._getItem(itemId, type);
  1889. if (!filter || filter(item)) {
  1890. items.push(item);
  1891. }
  1892. }
  1893. }
  1894. }
  1895. // order the results
  1896. if (options && options.order && id == undefined) {
  1897. this._sort(items, options.order);
  1898. }
  1899. // filter fields of the items
  1900. if (options && options.fields) {
  1901. var fields = options.fields;
  1902. if (id != undefined) {
  1903. item = this._filterFields(item, fields);
  1904. }
  1905. else {
  1906. for (i = 0, len = items.length; i < len; i++) {
  1907. items[i] = this._filterFields(items[i], fields);
  1908. }
  1909. }
  1910. }
  1911. // return the results
  1912. if (returnType == 'DataTable') {
  1913. var columns = this._getColumnNames(data);
  1914. if (id != undefined) {
  1915. // append a single item to the data table
  1916. me._appendRow(data, columns, item);
  1917. }
  1918. else {
  1919. // copy the items to the provided data table
  1920. for (i = 0, len = items.length; i < len; i++) {
  1921. me._appendRow(data, columns, items[i]);
  1922. }
  1923. }
  1924. return data;
  1925. }
  1926. else {
  1927. // return an array
  1928. if (id != undefined) {
  1929. // a single item
  1930. return item;
  1931. }
  1932. else {
  1933. // multiple items
  1934. if (data) {
  1935. // copy the items to the provided array
  1936. for (i = 0, len = items.length; i < len; i++) {
  1937. data.push(items[i]);
  1938. }
  1939. return data;
  1940. }
  1941. else {
  1942. // just return our array
  1943. return items;
  1944. }
  1945. }
  1946. }
  1947. };
  1948. /**
  1949. * Get ids of all items or from a filtered set of items.
  1950. * @param {Object} [options] An Object with options. Available options:
  1951. * {function} [filter] filter items
  1952. * {String | function} [order] Order the items by
  1953. * a field name or custom sort function.
  1954. * @return {Array} ids
  1955. */
  1956. DataSet.prototype.getIds = function (options) {
  1957. var data = this._data,
  1958. filter = options && options.filter,
  1959. order = options && options.order,
  1960. type = options && options.type || this._options.type,
  1961. i,
  1962. len,
  1963. id,
  1964. item,
  1965. items,
  1966. ids = [];
  1967. if (filter) {
  1968. // get filtered items
  1969. if (order) {
  1970. // create ordered list
  1971. items = [];
  1972. for (id in data) {
  1973. if (data.hasOwnProperty(id)) {
  1974. item = this._getItem(id, type);
  1975. if (filter(item)) {
  1976. items.push(item);
  1977. }
  1978. }
  1979. }
  1980. this._sort(items, order);
  1981. for (i = 0, len = items.length; i < len; i++) {
  1982. ids[i] = items[i][this._fieldId];
  1983. }
  1984. }
  1985. else {
  1986. // create unordered list
  1987. for (id in data) {
  1988. if (data.hasOwnProperty(id)) {
  1989. item = this._getItem(id, type);
  1990. if (filter(item)) {
  1991. ids.push(item[this._fieldId]);
  1992. }
  1993. }
  1994. }
  1995. }
  1996. }
  1997. else {
  1998. // get all items
  1999. if (order) {
  2000. // create an ordered list
  2001. items = [];
  2002. for (id in data) {
  2003. if (data.hasOwnProperty(id)) {
  2004. items.push(data[id]);
  2005. }
  2006. }
  2007. this._sort(items, order);
  2008. for (i = 0, len = items.length; i < len; i++) {
  2009. ids[i] = items[i][this._fieldId];
  2010. }
  2011. }
  2012. else {
  2013. // create unordered list
  2014. for (id in data) {
  2015. if (data.hasOwnProperty(id)) {
  2016. item = data[id];
  2017. ids.push(item[this._fieldId]);
  2018. }
  2019. }
  2020. }
  2021. }
  2022. return ids;
  2023. };
  2024. /**
  2025. * Execute a callback function for every item in the dataset.
  2026. * @param {function} callback
  2027. * @param {Object} [options] Available options:
  2028. * {Object.<String, String>} [type]
  2029. * {String[]} [fields] filter fields
  2030. * {function} [filter] filter items
  2031. * {String | function} [order] Order the items by
  2032. * a field name or custom sort function.
  2033. */
  2034. DataSet.prototype.forEach = function (callback, options) {
  2035. var filter = options && options.filter,
  2036. type = options && options.type || this._options.type,
  2037. data = this._data,
  2038. item,
  2039. id;
  2040. if (options && options.order) {
  2041. // execute forEach on ordered list
  2042. var items = this.get(options);
  2043. for (var i = 0, len = items.length; i < len; i++) {
  2044. item = items[i];
  2045. id = item[this._fieldId];
  2046. callback(item, id);
  2047. }
  2048. }
  2049. else {
  2050. // unordered
  2051. for (id in data) {
  2052. if (data.hasOwnProperty(id)) {
  2053. item = this._getItem(id, type);
  2054. if (!filter || filter(item)) {
  2055. callback(item, id);
  2056. }
  2057. }
  2058. }
  2059. }
  2060. };
  2061. /**
  2062. * Map every item in the dataset.
  2063. * @param {function} callback
  2064. * @param {Object} [options] Available options:
  2065. * {Object.<String, String>} [type]
  2066. * {String[]} [fields] filter fields
  2067. * {function} [filter] filter items
  2068. * {String | function} [order] Order the items by
  2069. * a field name or custom sort function.
  2070. * @return {Object[]} mappedItems
  2071. */
  2072. DataSet.prototype.map = function (callback, options) {
  2073. var filter = options && options.filter,
  2074. type = options && options.type || this._options.type,
  2075. mappedItems = [],
  2076. data = this._data,
  2077. item;
  2078. // convert and filter items
  2079. for (var id in data) {
  2080. if (data.hasOwnProperty(id)) {
  2081. item = this._getItem(id, type);
  2082. if (!filter || filter(item)) {
  2083. mappedItems.push(callback(item, id));
  2084. }
  2085. }
  2086. }
  2087. // order items
  2088. if (options && options.order) {
  2089. this._sort(mappedItems, options.order);
  2090. }
  2091. return mappedItems;
  2092. };
  2093. /**
  2094. * Filter the fields of an item
  2095. * @param {Object} item
  2096. * @param {String[]} fields Field names
  2097. * @return {Object} filteredItem
  2098. * @private
  2099. */
  2100. DataSet.prototype._filterFields = function (item, fields) {
  2101. var filteredItem = {};
  2102. for (var field in item) {
  2103. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  2104. filteredItem[field] = item[field];
  2105. }
  2106. }
  2107. return filteredItem;
  2108. };
  2109. /**
  2110. * Sort the provided array with items
  2111. * @param {Object[]} items
  2112. * @param {String | function} order A field name or custom sort function.
  2113. * @private
  2114. */
  2115. DataSet.prototype._sort = function (items, order) {
  2116. if (util.isString(order)) {
  2117. // order by provided field name
  2118. var name = order; // field name
  2119. items.sort(function (a, b) {
  2120. var av = a[name];
  2121. var bv = b[name];
  2122. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  2123. });
  2124. }
  2125. else if (typeof order === 'function') {
  2126. // order by sort function
  2127. items.sort(order);
  2128. }
  2129. // TODO: extend order by an Object {field:String, direction:String}
  2130. // where direction can be 'asc' or 'desc'
  2131. else {
  2132. throw new TypeError('Order must be a function or a string');
  2133. }
  2134. };
  2135. /**
  2136. * Remove an object by pointer or by id
  2137. * @param {String | Number | Object | Array} id Object or id, or an array with
  2138. * objects or ids to be removed
  2139. * @param {String} [senderId] Optional sender id
  2140. * @return {Array} removedIds
  2141. */
  2142. DataSet.prototype.remove = function (id, senderId) {
  2143. var removedIds = [],
  2144. i, len, removedId;
  2145. if (Array.isArray(id)) {
  2146. for (i = 0, len = id.length; i < len; i++) {
  2147. removedId = this._remove(id[i]);
  2148. if (removedId != null) {
  2149. removedIds.push(removedId);
  2150. }
  2151. }
  2152. }
  2153. else {
  2154. removedId = this._remove(id);
  2155. if (removedId != null) {
  2156. removedIds.push(removedId);
  2157. }
  2158. }
  2159. if (removedIds.length) {
  2160. this._trigger('remove', {items: removedIds}, senderId);
  2161. }
  2162. return removedIds;
  2163. };
  2164. /**
  2165. * Remove an item by its id
  2166. * @param {Number | String | Object} id id or item
  2167. * @returns {Number | String | null} id
  2168. * @private
  2169. */
  2170. DataSet.prototype._remove = function (id) {
  2171. if (util.isNumber(id) || util.isString(id)) {
  2172. if (this._data[id]) {
  2173. delete this._data[id];
  2174. return id;
  2175. }
  2176. }
  2177. else if (id instanceof Object) {
  2178. var itemId = id[this._fieldId];
  2179. if (itemId && this._data[itemId]) {
  2180. delete this._data[itemId];
  2181. return itemId;
  2182. }
  2183. }
  2184. return null;
  2185. };
  2186. /**
  2187. * Clear the data
  2188. * @param {String} [senderId] Optional sender id
  2189. * @return {Array} removedIds The ids of all removed items
  2190. */
  2191. DataSet.prototype.clear = function (senderId) {
  2192. var ids = Object.keys(this._data);
  2193. this._data = {};
  2194. this._trigger('remove', {items: ids}, senderId);
  2195. return ids;
  2196. };
  2197. /**
  2198. * Find the item with maximum value of a specified field
  2199. * @param {String} field
  2200. * @return {Object | null} item Item containing max value, or null if no items
  2201. */
  2202. DataSet.prototype.max = function (field) {
  2203. var data = this._data,
  2204. max = null,
  2205. maxField = null;
  2206. for (var id in data) {
  2207. if (data.hasOwnProperty(id)) {
  2208. var item = data[id];
  2209. var itemField = item[field];
  2210. if (itemField != null && (!max || itemField > maxField)) {
  2211. max = item;
  2212. maxField = itemField;
  2213. }
  2214. }
  2215. }
  2216. return max;
  2217. };
  2218. /**
  2219. * Find the item with minimum value of a specified field
  2220. * @param {String} field
  2221. * @return {Object | null} item Item containing max value, or null if no items
  2222. */
  2223. DataSet.prototype.min = function (field) {
  2224. var data = this._data,
  2225. min = null,
  2226. minField = null;
  2227. for (var id in data) {
  2228. if (data.hasOwnProperty(id)) {
  2229. var item = data[id];
  2230. var itemField = item[field];
  2231. if (itemField != null && (!min || itemField < minField)) {
  2232. min = item;
  2233. minField = itemField;
  2234. }
  2235. }
  2236. }
  2237. return min;
  2238. };
  2239. /**
  2240. * Find all distinct values of a specified field
  2241. * @param {String} field
  2242. * @return {Array} values Array containing all distinct values. If data items
  2243. * do not contain the specified field are ignored.
  2244. * The returned array is unordered.
  2245. */
  2246. DataSet.prototype.distinct = function (field) {
  2247. var data = this._data;
  2248. var values = [];
  2249. var fieldType = this._options.type && this._options.type[field] || null;
  2250. var count = 0;
  2251. var i;
  2252. for (var prop in data) {
  2253. if (data.hasOwnProperty(prop)) {
  2254. var item = data[prop];
  2255. var value = item[field];
  2256. var exists = false;
  2257. for (i = 0; i < count; i++) {
  2258. if (values[i] == value) {
  2259. exists = true;
  2260. break;
  2261. }
  2262. }
  2263. if (!exists && (value !== undefined)) {
  2264. values[count] = value;
  2265. count++;
  2266. }
  2267. }
  2268. }
  2269. if (fieldType) {
  2270. for (i = 0; i < values.length; i++) {
  2271. values[i] = util.convert(values[i], fieldType);
  2272. }
  2273. }
  2274. return values;
  2275. };
  2276. /**
  2277. * Add a single item. Will fail when an item with the same id already exists.
  2278. * @param {Object} item
  2279. * @return {String} id
  2280. * @private
  2281. */
  2282. DataSet.prototype._addItem = function (item) {
  2283. var id = item[this._fieldId];
  2284. if (id != undefined) {
  2285. // check whether this id is already taken
  2286. if (this._data[id]) {
  2287. // item already exists
  2288. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  2289. }
  2290. }
  2291. else {
  2292. // generate an id
  2293. id = util.randomUUID();
  2294. item[this._fieldId] = id;
  2295. }
  2296. var d = {};
  2297. for (var field in item) {
  2298. if (item.hasOwnProperty(field)) {
  2299. var fieldType = this._type[field]; // type may be undefined
  2300. d[field] = util.convert(item[field], fieldType);
  2301. }
  2302. }
  2303. this._data[id] = d;
  2304. return id;
  2305. };
  2306. /**
  2307. * Get an item. Fields can be converted to a specific type
  2308. * @param {String} id
  2309. * @param {Object.<String, String>} [types] field types to convert
  2310. * @return {Object | null} item
  2311. * @private
  2312. */
  2313. DataSet.prototype._getItem = function (id, types) {
  2314. var field, value;
  2315. // get the item from the dataset
  2316. var raw = this._data[id];
  2317. if (!raw) {
  2318. return null;
  2319. }
  2320. // convert the items field types
  2321. var converted = {};
  2322. if (types) {
  2323. for (field in raw) {
  2324. if (raw.hasOwnProperty(field)) {
  2325. value = raw[field];
  2326. converted[field] = util.convert(value, types[field]);
  2327. }
  2328. }
  2329. }
  2330. else {
  2331. // no field types specified, no converting needed
  2332. for (field in raw) {
  2333. if (raw.hasOwnProperty(field)) {
  2334. value = raw[field];
  2335. converted[field] = value;
  2336. }
  2337. }
  2338. }
  2339. return converted;
  2340. };
  2341. /**
  2342. * Update a single item: merge with existing item.
  2343. * Will fail when the item has no id, or when there does not exist an item
  2344. * with the same id.
  2345. * @param {Object} item
  2346. * @return {String} id
  2347. * @private
  2348. */
  2349. DataSet.prototype._updateItem = function (item) {
  2350. var id = item[this._fieldId];
  2351. if (id == undefined) {
  2352. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  2353. }
  2354. var d = this._data[id];
  2355. if (!d) {
  2356. // item doesn't exist
  2357. throw new Error('Cannot update item: no item with id ' + id + ' found');
  2358. }
  2359. // merge with current item
  2360. for (var field in item) {
  2361. if (item.hasOwnProperty(field)) {
  2362. var fieldType = this._type[field]; // type may be undefined
  2363. d[field] = util.convert(item[field], fieldType);
  2364. }
  2365. }
  2366. return id;
  2367. };
  2368. /**
  2369. * Get an array with the column names of a Google DataTable
  2370. * @param {DataTable} dataTable
  2371. * @return {String[]} columnNames
  2372. * @private
  2373. */
  2374. DataSet.prototype._getColumnNames = function (dataTable) {
  2375. var columns = [];
  2376. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  2377. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  2378. }
  2379. return columns;
  2380. };
  2381. /**
  2382. * Append an item as a row to the dataTable
  2383. * @param dataTable
  2384. * @param columns
  2385. * @param item
  2386. * @private
  2387. */
  2388. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  2389. var row = dataTable.addRow();
  2390. for (var col = 0, cols = columns.length; col < cols; col++) {
  2391. var field = columns[col];
  2392. dataTable.setValue(row, col, item[field]);
  2393. }
  2394. };
  2395. /**
  2396. * DataView
  2397. *
  2398. * a dataview offers a filtered view on a dataset or an other dataview.
  2399. *
  2400. * @param {DataSet | DataView} data
  2401. * @param {Object} [options] Available options: see method get
  2402. *
  2403. * @constructor DataView
  2404. */
  2405. function DataView (data, options) {
  2406. this._data = null;
  2407. this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
  2408. this._options = options || {};
  2409. this._fieldId = 'id'; // name of the field containing id
  2410. this._subscribers = {}; // event subscribers
  2411. var me = this;
  2412. this.listener = function () {
  2413. me._onEvent.apply(me, arguments);
  2414. };
  2415. this.setData(data);
  2416. }
  2417. // TODO: implement a function .config() to dynamically update things like configured filter
  2418. // and trigger changes accordingly
  2419. /**
  2420. * Set a data source for the view
  2421. * @param {DataSet | DataView} data
  2422. */
  2423. DataView.prototype.setData = function (data) {
  2424. var ids, i, len;
  2425. if (this._data) {
  2426. // unsubscribe from current dataset
  2427. if (this._data.unsubscribe) {
  2428. this._data.unsubscribe('*', this.listener);
  2429. }
  2430. // trigger a remove of all items in memory
  2431. ids = [];
  2432. for (var id in this._ids) {
  2433. if (this._ids.hasOwnProperty(id)) {
  2434. ids.push(id);
  2435. }
  2436. }
  2437. this._ids = {};
  2438. this._trigger('remove', {items: ids});
  2439. }
  2440. this._data = data;
  2441. if (this._data) {
  2442. // update fieldId
  2443. this._fieldId = this._options.fieldId ||
  2444. (this._data && this._data.options && this._data.options.fieldId) ||
  2445. 'id';
  2446. // trigger an add of all added items
  2447. ids = this._data.getIds({filter: this._options && this._options.filter});
  2448. for (i = 0, len = ids.length; i < len; i++) {
  2449. id = ids[i];
  2450. this._ids[id] = true;
  2451. }
  2452. this._trigger('add', {items: ids});
  2453. // subscribe to new dataset
  2454. if (this._data.on) {
  2455. this._data.on('*', this.listener);
  2456. }
  2457. }
  2458. };
  2459. /**
  2460. * Get data from the data view
  2461. *
  2462. * Usage:
  2463. *
  2464. * get()
  2465. * get(options: Object)
  2466. * get(options: Object, data: Array | DataTable)
  2467. *
  2468. * get(id: Number)
  2469. * get(id: Number, options: Object)
  2470. * get(id: Number, options: Object, data: Array | DataTable)
  2471. *
  2472. * get(ids: Number[])
  2473. * get(ids: Number[], options: Object)
  2474. * get(ids: Number[], options: Object, data: Array | DataTable)
  2475. *
  2476. * Where:
  2477. *
  2478. * {Number | String} id The id of an item
  2479. * {Number[] | String{}} ids An array with ids of items
  2480. * {Object} options An Object with options. Available options:
  2481. * {String} [type] Type of data to be returned. Can
  2482. * be 'DataTable' or 'Array' (default)
  2483. * {Object.<String, String>} [convert]
  2484. * {String[]} [fields] field names to be returned
  2485. * {function} [filter] filter items
  2486. * {String | function} [order] Order the items by
  2487. * a field name or custom sort function.
  2488. * {Array | DataTable} [data] If provided, items will be appended to this
  2489. * array or table. Required in case of Google
  2490. * DataTable.
  2491. * @param args
  2492. */
  2493. DataView.prototype.get = function (args) {
  2494. var me = this;
  2495. // parse the arguments
  2496. var ids, options, data;
  2497. var firstType = util.getType(arguments[0]);
  2498. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2499. // get(id(s) [, options] [, data])
  2500. ids = arguments[0]; // can be a single id or an array with ids
  2501. options = arguments[1];
  2502. data = arguments[2];
  2503. }
  2504. else {
  2505. // get([, options] [, data])
  2506. options = arguments[0];
  2507. data = arguments[1];
  2508. }
  2509. // extend the options with the default options and provided options
  2510. var viewOptions = util.extend({}, this._options, options);
  2511. // create a combined filter method when needed
  2512. if (this._options.filter && options && options.filter) {
  2513. viewOptions.filter = function (item) {
  2514. return me._options.filter(item) && options.filter(item);
  2515. }
  2516. }
  2517. // build up the call to the linked data set
  2518. var getArguments = [];
  2519. if (ids != undefined) {
  2520. getArguments.push(ids);
  2521. }
  2522. getArguments.push(viewOptions);
  2523. getArguments.push(data);
  2524. return this._data && this._data.get.apply(this._data, getArguments);
  2525. };
  2526. /**
  2527. * Get ids of all items or from a filtered set of items.
  2528. * @param {Object} [options] An Object with options. Available options:
  2529. * {function} [filter] filter items
  2530. * {String | function} [order] Order the items by
  2531. * a field name or custom sort function.
  2532. * @return {Array} ids
  2533. */
  2534. DataView.prototype.getIds = function (options) {
  2535. var ids;
  2536. if (this._data) {
  2537. var defaultFilter = this._options.filter;
  2538. var filter;
  2539. if (options && options.filter) {
  2540. if (defaultFilter) {
  2541. filter = function (item) {
  2542. return defaultFilter(item) && options.filter(item);
  2543. }
  2544. }
  2545. else {
  2546. filter = options.filter;
  2547. }
  2548. }
  2549. else {
  2550. filter = defaultFilter;
  2551. }
  2552. ids = this._data.getIds({
  2553. filter: filter,
  2554. order: options && options.order
  2555. });
  2556. }
  2557. else {
  2558. ids = [];
  2559. }
  2560. return ids;
  2561. };
  2562. /**
  2563. * Event listener. Will propagate all events from the connected data set to
  2564. * the subscribers of the DataView, but will filter the items and only trigger
  2565. * when there are changes in the filtered data set.
  2566. * @param {String} event
  2567. * @param {Object | null} params
  2568. * @param {String} senderId
  2569. * @private
  2570. */
  2571. DataView.prototype._onEvent = function (event, params, senderId) {
  2572. var i, len, id, item,
  2573. ids = params && params.items,
  2574. data = this._data,
  2575. added = [],
  2576. updated = [],
  2577. removed = [];
  2578. if (ids && data) {
  2579. switch (event) {
  2580. case 'add':
  2581. // filter the ids of the added items
  2582. for (i = 0, len = ids.length; i < len; i++) {
  2583. id = ids[i];
  2584. item = this.get(id);
  2585. if (item) {
  2586. this._ids[id] = true;
  2587. added.push(id);
  2588. }
  2589. }
  2590. break;
  2591. case 'update':
  2592. // determine the event from the views viewpoint: an updated
  2593. // item can be added, updated, or removed from this view.
  2594. for (i = 0, len = ids.length; i < len; i++) {
  2595. id = ids[i];
  2596. item = this.get(id);
  2597. if (item) {
  2598. if (this._ids[id]) {
  2599. updated.push(id);
  2600. }
  2601. else {
  2602. this._ids[id] = true;
  2603. added.push(id);
  2604. }
  2605. }
  2606. else {
  2607. if (this._ids[id]) {
  2608. delete this._ids[id];
  2609. removed.push(id);
  2610. }
  2611. else {
  2612. // nothing interesting for me :-(
  2613. }
  2614. }
  2615. }
  2616. break;
  2617. case 'remove':
  2618. // filter the ids of the removed items
  2619. for (i = 0, len = ids.length; i < len; i++) {
  2620. id = ids[i];
  2621. if (this._ids[id]) {
  2622. delete this._ids[id];
  2623. removed.push(id);
  2624. }
  2625. }
  2626. break;
  2627. }
  2628. if (added.length) {
  2629. this._trigger('add', {items: added}, senderId);
  2630. }
  2631. if (updated.length) {
  2632. this._trigger('update', {items: updated}, senderId);
  2633. }
  2634. if (removed.length) {
  2635. this._trigger('remove', {items: removed}, senderId);
  2636. }
  2637. }
  2638. };
  2639. // copy subscription functionality from DataSet
  2640. DataView.prototype.on = DataSet.prototype.on;
  2641. DataView.prototype.off = DataSet.prototype.off;
  2642. DataView.prototype._trigger = DataSet.prototype._trigger;
  2643. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2644. DataView.prototype.subscribe = DataView.prototype.on;
  2645. DataView.prototype.unsubscribe = DataView.prototype.off;
  2646. /**
  2647. * @constructor Group
  2648. * @param {Number | String} groupId
  2649. * @param {Object} data
  2650. * @param {ItemSet} itemSet
  2651. */
  2652. function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
  2653. this.id = groupId;
  2654. var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
  2655. this.options = util.selectiveBridgeObject(fields,options);
  2656. this.usingDefaultStyle = group.className === undefined;
  2657. this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
  2658. this.zeroPosition = 0;
  2659. this.update(group);
  2660. if (this.usingDefaultStyle == true) {
  2661. this.groupsUsingDefaultStyles[0] += 1;
  2662. }
  2663. this.itemsData = [];
  2664. }
  2665. GraphGroup.prototype.setItems = function(items) {
  2666. if (items != null) {
  2667. this.itemsData = items;
  2668. if (this.options.sort == true) {
  2669. this.itemsData.sort(function (a,b) {return a.x - b.x;})
  2670. }
  2671. }
  2672. else {
  2673. this.itemsData = [];
  2674. }
  2675. }
  2676. GraphGroup.prototype.setZeroPosition = function(pos) {
  2677. this.zeroPosition = pos;
  2678. }
  2679. GraphGroup.prototype.setOptions = function(options) {
  2680. if (options !== undefined) {
  2681. var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
  2682. util.selectiveDeepExtend(fields, this.options, options);
  2683. util.mergeOptions(this.options, options,'catmullRom');
  2684. util.mergeOptions(this.options, options,'drawPoints');
  2685. util.mergeOptions(this.options, options,'shaded');
  2686. if (options.catmullRom) {
  2687. if (typeof options.catmullRom == 'object') {
  2688. if (options.catmullRom.parametrization) {
  2689. if (options.catmullRom.parametrization == 'uniform') {
  2690. this.options.catmullRom.alpha = 0;
  2691. }
  2692. else if (options.catmullRom.parametrization == 'chordal') {
  2693. this.options.catmullRom.alpha = 1.0;
  2694. }
  2695. else {
  2696. this.options.catmullRom.parametrization = 'centripetal';
  2697. this.options.catmullRom.alpha = 0.5;
  2698. }
  2699. }
  2700. }
  2701. }
  2702. }
  2703. };
  2704. GraphGroup.prototype.update = function(group) {
  2705. this.group = group;
  2706. this.content = group.content || 'graph';
  2707. this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
  2708. this.setOptions(group.options);
  2709. };
  2710. GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
  2711. var fillHeight = iconHeight * 0.5;
  2712. var path, fillPath;
  2713. var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
  2714. outline.setAttributeNS(null, "x", x);
  2715. outline.setAttributeNS(null, "y", y - fillHeight);
  2716. outline.setAttributeNS(null, "width", iconWidth);
  2717. outline.setAttributeNS(null, "height", 2*fillHeight);
  2718. outline.setAttributeNS(null, "class", "outline");
  2719. if (this.options.style == 'line') {
  2720. path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
  2721. path.setAttributeNS(null, "class", this.className);
  2722. path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
  2723. if (this.options.shaded.enabled == true) {
  2724. fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
  2725. if (this.options.shaded.orientation == 'top') {
  2726. fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
  2727. "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
  2728. }
  2729. else {
  2730. fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
  2731. "L"+x+"," + (y + fillHeight) + " " +
  2732. "L"+ (x + iconWidth) + "," + (y + fillHeight) +
  2733. "L"+ (x + iconWidth) + ","+y);
  2734. }
  2735. fillPath.setAttributeNS(null, "class", this.className + " iconFill");
  2736. }
  2737. if (this.options.drawPoints.enabled == true) {
  2738. DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
  2739. }
  2740. }
  2741. else {
  2742. var barWidth = Math.round(0.3 * iconWidth);
  2743. var bar1Height = Math.round(0.4 * iconHeight);
  2744. var bar2Height = Math.round(0.75 * iconHeight);
  2745. var offset = Math.round((iconWidth - (2 * barWidth))/3);
  2746. DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
  2747. DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
  2748. }
  2749. }
  2750. /**
  2751. * Created by Alex on 6/17/14.
  2752. */
  2753. function Legend(body, options, side) {
  2754. this.body = body;
  2755. this.defaultOptions = {
  2756. enabled: true,
  2757. icons: true,
  2758. iconSize: 20,
  2759. iconSpacing: 6,
  2760. left: {
  2761. visible: true,
  2762. position: 'top-left' // top/bottom - left,center,right
  2763. },
  2764. right: {
  2765. visible: true,
  2766. position: 'top-left' // top/bottom - left,center,right
  2767. }
  2768. }
  2769. this.side = side;
  2770. this.options = util.extend({},this.defaultOptions);
  2771. this.svgElements = {};
  2772. this.dom = {};
  2773. this.groups = {};
  2774. this.amountOfGroups = 0;
  2775. this._create();
  2776. this.setOptions(options);
  2777. };
  2778. Legend.prototype = new Component();
  2779. Legend.prototype.addGroup = function(label, graphOptions) {
  2780. if (!this.groups.hasOwnProperty(label)) {
  2781. this.groups[label] = graphOptions;
  2782. }
  2783. this.amountOfGroups += 1;
  2784. };
  2785. Legend.prototype.updateGroup = function(label, graphOptions) {
  2786. this.groups[label] = graphOptions;
  2787. };
  2788. Legend.prototype.removeGroup = function(label) {
  2789. if (this.groups.hasOwnProperty(label)) {
  2790. delete this.groups[label];
  2791. this.amountOfGroups -= 1;
  2792. }
  2793. };
  2794. Legend.prototype._create = function() {
  2795. this.dom.frame = document.createElement('div');
  2796. this.dom.frame.className = 'legend';
  2797. this.dom.frame.style.position = "absolute";
  2798. this.dom.frame.style.top = "10px";
  2799. this.dom.frame.style.display = "block";
  2800. this.dom.textArea = document.createElement('div');
  2801. this.dom.textArea.className = 'legendText';
  2802. this.dom.textArea.style.position = "relative";
  2803. this.dom.textArea.style.top = "0px";
  2804. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  2805. this.svg.style.position = 'absolute';
  2806. this.svg.style.top = 0 +'px';
  2807. this.svg.style.width = this.options.iconSize + 5 + 'px';
  2808. this.dom.frame.appendChild(this.svg);
  2809. this.dom.frame.appendChild(this.dom.textArea);
  2810. }
  2811. /**
  2812. * Hide the component from the DOM
  2813. */
  2814. Legend.prototype.hide = function() {
  2815. // remove the frame containing the items
  2816. if (this.dom.frame.parentNode) {
  2817. this.dom.frame.parentNode.removeChild(this.dom.frame);
  2818. }
  2819. };
  2820. /**
  2821. * Show the component in the DOM (when not already visible).
  2822. * @return {Boolean} changed
  2823. */
  2824. Legend.prototype.show = function() {
  2825. // show frame containing the items
  2826. if (!this.dom.frame.parentNode) {
  2827. this.body.dom.center.appendChild(this.dom.frame);
  2828. }
  2829. };
  2830. Legend.prototype.setOptions = function(options) {
  2831. var fields = ['enabled','orientation','icons','left','right'];
  2832. util.selectiveDeepExtend(fields, this.options, options);
  2833. }
  2834. Legend.prototype.redraw = function() {
  2835. if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false) {
  2836. this.hide();
  2837. }
  2838. else {
  2839. this.show();
  2840. if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
  2841. this.dom.frame.style.left = '4px';
  2842. this.dom.frame.style.textAlign = "left";
  2843. this.dom.textArea.style.textAlign = "left";
  2844. this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
  2845. this.dom.textArea.style.right = '';
  2846. this.svg.style.left = 0 +'px';
  2847. this.svg.style.right = '';
  2848. }
  2849. else {
  2850. this.dom.frame.style.right = '4px';
  2851. this.dom.frame.style.textAlign = "right";
  2852. this.dom.textArea.style.textAlign = "right";
  2853. this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
  2854. this.dom.textArea.style.left = '';
  2855. this.svg.style.right = 0 +'px';
  2856. this.svg.style.left = '';
  2857. }
  2858. if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
  2859. this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
  2860. this.dom.frame.style.bottom = '';
  2861. }
  2862. else {
  2863. this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
  2864. this.dom.frame.style.top = '';
  2865. }
  2866. if (this.options.icons == false) {
  2867. this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
  2868. this.dom.textArea.style.right = '';
  2869. this.dom.textArea.style.left = '';
  2870. this.svg.style.width = '0px';
  2871. }
  2872. else {
  2873. this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
  2874. this.drawLegendIcons();
  2875. }
  2876. var content = "";
  2877. for (var groupId in this.groups) {
  2878. if (this.groups.hasOwnProperty(groupId)) {
  2879. content += this.groups[groupId].content + '<br />';
  2880. }
  2881. }
  2882. this.dom.textArea.innerHTML = content;
  2883. this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
  2884. }
  2885. }
  2886. Legend.prototype.drawLegendIcons = function() {
  2887. if (this.dom.frame.parentNode) {
  2888. DOMutil.prepareElements(this.svgElements);
  2889. var padding = window.getComputedStyle(this.dom.frame).paddingTop;
  2890. var iconOffset = Number(padding.replace("px",''));
  2891. var x = iconOffset;
  2892. var iconWidth = this.options.iconSize;
  2893. var iconHeight = 0.75 * this.options.iconSize;
  2894. var y = iconOffset + 0.5 * iconHeight + 3;
  2895. this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
  2896. for (var groupId in this.groups) {
  2897. if (this.groups.hasOwnProperty(groupId)) {
  2898. this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
  2899. y += iconHeight + this.options.iconSpacing;
  2900. }
  2901. }
  2902. DOMutil.cleanupElements(this.svgElements);
  2903. }
  2904. }
  2905. /**
  2906. * A horizontal time axis
  2907. * @param {Object} [options] See DataAxis.setOptions for the available
  2908. * options.
  2909. * @constructor DataAxis
  2910. * @extends Component
  2911. * @param body
  2912. */
  2913. function DataAxis (body, options, svg) {
  2914. this.id = util.randomUUID();
  2915. this.body = body;
  2916. this.defaultOptions = {
  2917. orientation: 'left', // supported: 'left', 'right'
  2918. showMinorLabels: true,
  2919. showMajorLabels: true,
  2920. icons: true,
  2921. majorLinesOffset: 7,
  2922. minorLinesOffset: 4,
  2923. labelOffsetX: 10,
  2924. labelOffsetY: 2,
  2925. iconWidth: 20,
  2926. width: '40px',
  2927. visible: true
  2928. };
  2929. this.linegraphSVG = svg;
  2930. this.props = {};
  2931. this.DOMelements = { // dynamic elements
  2932. lines: {},
  2933. labels: {}
  2934. };
  2935. this.dom = {};
  2936. this.range = {start:0, end:0};
  2937. this.options = util.extend({}, this.defaultOptions);
  2938. this.conversionFactor = 1;
  2939. this.setOptions(options);
  2940. this.width = Number(('' + this.options.width).replace("px",""));
  2941. this.minWidth = this.width;
  2942. this.height = this.linegraphSVG.offsetHeight;
  2943. this.stepPixels = 25;
  2944. this.stepPixelsForced = 25;
  2945. this.lineOffset = 0;
  2946. this.master = true;
  2947. this.svgElements = {};
  2948. this.groups = {};
  2949. this.amountOfGroups = 0;
  2950. // create the HTML DOM
  2951. this._create();
  2952. }
  2953. DataAxis.prototype = new Component();
  2954. DataAxis.prototype.addGroup = function(label, graphOptions) {
  2955. if (!this.groups.hasOwnProperty(label)) {
  2956. this.groups[label] = graphOptions;
  2957. }
  2958. this.amountOfGroups += 1;
  2959. };
  2960. DataAxis.prototype.updateGroup = function(label, graphOptions) {
  2961. this.groups[label] = graphOptions;
  2962. };
  2963. DataAxis.prototype.removeGroup = function(label) {
  2964. if (this.groups.hasOwnProperty(label)) {
  2965. delete this.groups[label];
  2966. this.amountOfGroups -= 1;
  2967. }
  2968. };
  2969. DataAxis.prototype.setOptions = function (options) {
  2970. if (options) {
  2971. var redraw = false;
  2972. if (this.options.orientation != options.orientation && options.orientation !== undefined) {
  2973. redraw = true;
  2974. }
  2975. var fields = [
  2976. 'orientation',
  2977. 'showMinorLabels',
  2978. 'showMajorLabels',
  2979. 'icons',
  2980. 'majorLinesOffset',
  2981. 'minorLinesOffset',
  2982. 'labelOffsetX',
  2983. 'labelOffsetY',
  2984. 'iconWidth',
  2985. 'width',
  2986. 'visible'];
  2987. util.selectiveExtend(fields, this.options, options);
  2988. this.minWidth = Number(('' + this.options.width).replace("px",""));
  2989. if (redraw == true && this.dom.frame) {
  2990. this.hide();
  2991. this.show();
  2992. }
  2993. }
  2994. };
  2995. /**
  2996. * Create the HTML DOM for the DataAxis
  2997. */
  2998. DataAxis.prototype._create = function() {
  2999. this.dom.frame = document.createElement('div');
  3000. this.dom.frame.style.width = this.options.width;
  3001. this.dom.frame.style.height = this.height;
  3002. this.dom.lineContainer = document.createElement('div');
  3003. this.dom.lineContainer.style.width = '100%';
  3004. this.dom.lineContainer.style.height = this.height;
  3005. // create svg element for graph drawing.
  3006. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  3007. this.svg.style.position = "absolute";
  3008. this.svg.style.top = '0px';
  3009. this.svg.style.height = '100%';
  3010. this.svg.style.width = '100%';
  3011. this.svg.style.display = "block";
  3012. this.dom.frame.appendChild(this.svg);
  3013. };
  3014. DataAxis.prototype._redrawGroupIcons = function () {
  3015. DOMutil.prepareElements(this.svgElements);
  3016. var x;
  3017. var iconWidth = this.options.iconWidth;
  3018. var iconHeight = 15;
  3019. var iconOffset = 4;
  3020. var y = iconOffset + 0.5 * iconHeight;
  3021. if (this.options.orientation == 'left') {
  3022. x = iconOffset;
  3023. }
  3024. else {
  3025. x = this.width - iconWidth - iconOffset;
  3026. }
  3027. for (var groupId in this.groups) {
  3028. if (this.groups.hasOwnProperty(groupId)) {
  3029. this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
  3030. y += iconHeight + iconOffset;
  3031. }
  3032. }
  3033. DOMutil.cleanupElements(this.svgElements);
  3034. };
  3035. /**
  3036. * Create the HTML DOM for the DataAxis
  3037. */
  3038. DataAxis.prototype.show = function() {
  3039. if (!this.dom.frame.parentNode) {
  3040. if (this.options.orientation == 'left') {
  3041. this.body.dom.left.appendChild(this.dom.frame);
  3042. }
  3043. else {
  3044. this.body.dom.right.appendChild(this.dom.frame);
  3045. }
  3046. }
  3047. if (!this.dom.lineContainer.parentNode) {
  3048. this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
  3049. }
  3050. };
  3051. /**
  3052. * Create the HTML DOM for the DataAxis
  3053. */
  3054. DataAxis.prototype.hide = function() {
  3055. if (this.dom.frame.parentNode) {
  3056. this.dom.frame.parentNode.removeChild(this.dom.frame);
  3057. }
  3058. if (this.dom.lineContainer.parentNode) {
  3059. this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
  3060. }
  3061. };
  3062. /**
  3063. * Set a range (start and end)
  3064. * @param end
  3065. * @param start
  3066. * @param end
  3067. */
  3068. DataAxis.prototype.setRange = function (start, end) {
  3069. this.range.start = start;
  3070. this.range.end = end;
  3071. };
  3072. /**
  3073. * Repaint the component
  3074. * @return {boolean} Returns true if the component is resized
  3075. */
  3076. DataAxis.prototype.redraw = function () {
  3077. var changeCalled = false;
  3078. if (this.amountOfGroups == 0) {
  3079. this.hide();
  3080. }
  3081. else {
  3082. this.show();
  3083. this.height = Number(this.linegraphSVG.style.height.replace("px",""));
  3084. // svg offsetheight did not work in firefox and explorer...
  3085. this.dom.lineContainer.style.height = this.height + 'px';
  3086. this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
  3087. var props = this.props;
  3088. var frame = this.dom.frame;
  3089. // update classname
  3090. frame.className = 'dataaxis';
  3091. // calculate character width and height
  3092. this._calculateCharSize();
  3093. var orientation = this.options.orientation;
  3094. var showMinorLabels = this.options.showMinorLabels;
  3095. var showMajorLabels = this.options.showMajorLabels;
  3096. // determine the width and height of the elemens for the axis
  3097. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3098. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3099. props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
  3100. props.minorLineHeight = 1;
  3101. props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
  3102. props.majorLineHeight = 1;
  3103. // take frame offline while updating (is almost twice as fast)
  3104. if (orientation == 'left') {
  3105. frame.style.top = '0';
  3106. frame.style.left = '0';
  3107. frame.style.bottom = '';
  3108. frame.style.width = this.width + 'px';
  3109. frame.style.height = this.height + "px";
  3110. }
  3111. else { // right
  3112. frame.style.top = '';
  3113. frame.style.bottom = '0';
  3114. frame.style.left = '0';
  3115. frame.style.width = this.width + 'px';
  3116. frame.style.height = this.height + "px";
  3117. }
  3118. changeCalled = this._redrawLabels();
  3119. if (this.options.icons == true) {
  3120. this._redrawGroupIcons();
  3121. }
  3122. }
  3123. return changeCalled;
  3124. };
  3125. /**
  3126. * Repaint major and minor text labels and vertical grid lines
  3127. * @private
  3128. */
  3129. DataAxis.prototype._redrawLabels = function () {
  3130. DOMutil.prepareElements(this.DOMelements);
  3131. var orientation = this.options['orientation'];
  3132. // calculate range and step (step such that we have space for 7 characters per label)
  3133. var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
  3134. var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight);
  3135. this.step = step;
  3136. step.first();
  3137. // get the distance in pixels for a step
  3138. var stepPixels = this.dom.frame.offsetHeight / ((step.marginRange / step.step) + 1);
  3139. this.stepPixels = stepPixels;
  3140. var amountOfSteps = this.height / stepPixels;
  3141. var stepDifference = 0;
  3142. if (this.master == false) {
  3143. stepPixels = this.stepPixelsForced;
  3144. stepDifference = Math.round((this.height / stepPixels) - amountOfSteps);
  3145. for (var i = 0; i < 0.5 * stepDifference; i++) {
  3146. step.previous();
  3147. }
  3148. amountOfSteps = this.height / stepPixels;
  3149. }
  3150. this.valueAtZero = step.marginEnd;
  3151. var marginStartPos = 0;
  3152. // do not draw the first label
  3153. var max = 1;
  3154. step.next();
  3155. this.maxLabelSize = 0;
  3156. var y = 0;
  3157. while (max < Math.round(amountOfSteps)) {
  3158. y = Math.round(max * stepPixels);
  3159. marginStartPos = max * stepPixels;
  3160. var isMajor = step.isMajor();
  3161. if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
  3162. this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
  3163. }
  3164. if (isMajor && this.options['showMajorLabels'] && this.master == true ||
  3165. this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
  3166. if (y >= 0) {
  3167. this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
  3168. }
  3169. this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
  3170. }
  3171. else {
  3172. this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
  3173. }
  3174. step.next();
  3175. max++;
  3176. }
  3177. this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step);
  3178. var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
  3179. // this will resize the yAxis to accomodate the labels.
  3180. if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
  3181. this.width = this.maxLabelSize + offset;
  3182. this.options.width = this.width + "px";
  3183. DOMutil.cleanupElements(this.DOMelements);
  3184. this.redraw();
  3185. return true;
  3186. }
  3187. // this will resize the yAxis if it is too big for the labels.
  3188. else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
  3189. this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
  3190. this.options.width = this.width + "px";
  3191. DOMutil.cleanupElements(this.DOMelements);
  3192. this.redraw();
  3193. return true;
  3194. }
  3195. else {
  3196. DOMutil.cleanupElements(this.DOMelements);
  3197. return false;
  3198. }
  3199. };
  3200. /**
  3201. * Create a label for the axis at position x
  3202. * @private
  3203. * @param y
  3204. * @param text
  3205. * @param orientation
  3206. * @param className
  3207. * @param characterHeight
  3208. */
  3209. DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
  3210. // reuse redundant label
  3211. var label = DOMutil.getDOMElement('div',this.DOMelements, this.dom.frame); //this.dom.redundant.labels.shift();
  3212. label.className = className;
  3213. label.innerHTML = text;
  3214. if (orientation == 'left') {
  3215. label.style.left = '-' + this.options.labelOffsetX + 'px';
  3216. label.style.textAlign = "right";
  3217. }
  3218. else {
  3219. label.style.right = '-' + this.options.labelOffsetX + 'px';
  3220. label.style.textAlign = "left";
  3221. }
  3222. label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
  3223. text += '';
  3224. var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
  3225. if (this.maxLabelSize < text.length * largestWidth) {
  3226. this.maxLabelSize = text.length * largestWidth;
  3227. }
  3228. };
  3229. /**
  3230. * Create a minor line for the axis at position y
  3231. * @param y
  3232. * @param orientation
  3233. * @param className
  3234. * @param offset
  3235. * @param width
  3236. */
  3237. DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
  3238. if (this.master == true) {
  3239. var line = DOMutil.getDOMElement('div',this.DOMelements, this.dom.lineContainer);//this.dom.redundant.lines.shift();
  3240. line.className = className;
  3241. line.innerHTML = '';
  3242. if (orientation == 'left') {
  3243. line.style.left = (this.width - offset) + 'px';
  3244. }
  3245. else {
  3246. line.style.right = (this.width - offset) + 'px';
  3247. }
  3248. line.style.width = width + 'px';
  3249. line.style.top = y + 'px';
  3250. }
  3251. };
  3252. DataAxis.prototype.convertValue = function (value) {
  3253. var invertedValue = this.valueAtZero - value;
  3254. var convertedValue = invertedValue * this.conversionFactor;
  3255. return convertedValue; // the -2 is to compensate for the borders
  3256. };
  3257. /**
  3258. * Determine the size of text on the axis (both major and minor axis).
  3259. * The size is calculated only once and then cached in this.props.
  3260. * @private
  3261. */
  3262. DataAxis.prototype._calculateCharSize = function () {
  3263. // determine the char width and height on the minor axis
  3264. if (!('minorCharHeight' in this.props)) {
  3265. var textMinor = document.createTextNode('0');
  3266. var measureCharMinor = document.createElement('DIV');
  3267. measureCharMinor.className = 'yAxis minor measure';
  3268. measureCharMinor.appendChild(textMinor);
  3269. this.dom.frame.appendChild(measureCharMinor);
  3270. this.props.minorCharHeight = measureCharMinor.clientHeight;
  3271. this.props.minorCharWidth = measureCharMinor.clientWidth;
  3272. this.dom.frame.removeChild(measureCharMinor);
  3273. }
  3274. if (!('majorCharHeight' in this.props)) {
  3275. var textMajor = document.createTextNode('0');
  3276. var measureCharMajor = document.createElement('DIV');
  3277. measureCharMajor.className = 'yAxis major measure';
  3278. measureCharMajor.appendChild(textMajor);
  3279. this.dom.frame.appendChild(measureCharMajor);
  3280. this.props.majorCharHeight = measureCharMajor.clientHeight;
  3281. this.props.majorCharWidth = measureCharMajor.clientWidth;
  3282. this.dom.frame.removeChild(measureCharMajor);
  3283. }
  3284. };
  3285. /**
  3286. * Snap a date to a rounded value.
  3287. * The snap intervals are dependent on the current scale and step.
  3288. * @param {Date} date the date to be snapped.
  3289. * @return {Date} snappedDate
  3290. */
  3291. DataAxis.prototype.snap = function(date) {
  3292. return this.step.snap(date);
  3293. };
  3294. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  3295. /**
  3296. * This is the constructor of the LineGraph. It requires a Timeline body and options.
  3297. *
  3298. * @param body
  3299. * @param options
  3300. * @constructor
  3301. */
  3302. function LineGraph(body, options) {
  3303. this.id = util.randomUUID();
  3304. this.body = body;
  3305. this.defaultOptions = {
  3306. yAxisOrientation: 'left',
  3307. defaultGroup: 'default',
  3308. sort: true,
  3309. sampling: true,
  3310. graphHeight: '400px',
  3311. shaded: {
  3312. enabled: false,
  3313. orientation: 'bottom' // top, bottom
  3314. },
  3315. style: 'line', // line, bar
  3316. barChart: {
  3317. width: 50,
  3318. align: 'center' // left, center, right
  3319. },
  3320. catmullRom: {
  3321. enabled: true,
  3322. parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
  3323. alpha: 0.5
  3324. },
  3325. drawPoints: {
  3326. enabled: true,
  3327. size: 6,
  3328. style: 'square' // square, circle
  3329. },
  3330. dataAxis: {
  3331. showMinorLabels: true,
  3332. showMajorLabels: true,
  3333. icons: false,
  3334. width: '40px',
  3335. visible: true
  3336. },
  3337. legend: {
  3338. enabled: false,
  3339. icons: true,
  3340. left: {
  3341. visible: true,
  3342. position: 'top-left' // top/bottom - left,right
  3343. },
  3344. right: {
  3345. visible: true,
  3346. position: 'top-right' // top/bottom - left,right
  3347. }
  3348. }
  3349. };
  3350. // options is shared by this ItemSet and all its items
  3351. this.options = util.extend({}, this.defaultOptions);
  3352. this.dom = {};
  3353. this.props = {};
  3354. this.hammer = null;
  3355. this.groups = {};
  3356. var me = this;
  3357. this.itemsData = null; // DataSet
  3358. this.groupsData = null; // DataSet
  3359. // listeners for the DataSet of the items
  3360. this.itemListeners = {
  3361. 'add': function (event, params, senderId) {
  3362. me._onAdd(params.items);
  3363. },
  3364. 'update': function (event, params, senderId) {
  3365. me._onUpdate(params.items);
  3366. },
  3367. 'remove': function (event, params, senderId) {
  3368. me._onRemove(params.items);
  3369. }
  3370. };
  3371. // listeners for the DataSet of the groups
  3372. this.groupListeners = {
  3373. 'add': function (event, params, senderId) {
  3374. me._onAddGroups(params.items);
  3375. },
  3376. 'update': function (event, params, senderId) {
  3377. me._onUpdateGroups(params.items);
  3378. },
  3379. 'remove': function (event, params, senderId) {
  3380. me._onRemoveGroups(params.items);
  3381. }
  3382. };
  3383. this.items = {}; // object with an Item for every data item
  3384. this.selection = []; // list with the ids of all selected nodes
  3385. this.lastStart = this.body.range.start;
  3386. this.touchParams = {}; // stores properties while dragging
  3387. this.svgElements = {};
  3388. this.setOptions(options);
  3389. this.groupsUsingDefaultStyles = [0];
  3390. this.body.emitter.on("rangechange",function() {
  3391. if (me.lastStart != 0) {
  3392. var offset = me.body.range.start - me.lastStart;
  3393. var range = me.body.range.end - me.body.range.start;
  3394. if (me.width != 0) {
  3395. var rangePerPixelInv = me.width/range;
  3396. var xOffset = offset * rangePerPixelInv;
  3397. me.svg.style.left = (-me.width - xOffset) + "px";
  3398. }
  3399. }
  3400. });
  3401. this.body.emitter.on("rangechanged", function() {
  3402. me.lastStart = me.body.range.start;
  3403. me.svg.style.left = util.option.asSize(-me.width);
  3404. me._updateGraph.apply(me);
  3405. });
  3406. // create the HTML DOM
  3407. this._create();
  3408. this.body.emitter.emit("change");
  3409. }
  3410. LineGraph.prototype = new Component();
  3411. /**
  3412. * Create the HTML DOM for the ItemSet
  3413. */
  3414. LineGraph.prototype._create = function(){
  3415. var frame = document.createElement('div');
  3416. frame.className = 'LineGraph';
  3417. this.dom.frame = frame;
  3418. // create svg element for graph drawing.
  3419. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  3420. this.svg.style.position = "relative";
  3421. this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
  3422. this.svg.style.display = "block";
  3423. frame.appendChild(this.svg);
  3424. // data axis
  3425. this.options.dataAxis.orientation = 'left';
  3426. this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg);
  3427. this.options.dataAxis.orientation = 'right';
  3428. this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg);
  3429. delete this.options.dataAxis.orientation;
  3430. // legends
  3431. this.legendLeft = new Legend(this.body, this.options.legend, 'left');
  3432. this.legendRight = new Legend(this.body, this.options.legend, 'right');
  3433. this.show();
  3434. };
  3435. /**
  3436. * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
  3437. * @param options
  3438. */
  3439. LineGraph.prototype.setOptions = function(options) {
  3440. if (options) {
  3441. var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort'];
  3442. util.selectiveDeepExtend(fields, this.options, options);
  3443. util.mergeOptions(this.options, options,'catmullRom');
  3444. util.mergeOptions(this.options, options,'drawPoints');
  3445. util.mergeOptions(this.options, options,'shaded');
  3446. util.mergeOptions(this.options, options,'legend');
  3447. if (options.catmullRom) {
  3448. if (typeof options.catmullRom == 'object') {
  3449. if (options.catmullRom.parametrization) {
  3450. if (options.catmullRom.parametrization == 'uniform') {
  3451. this.options.catmullRom.alpha = 0;
  3452. }
  3453. else if (options.catmullRom.parametrization == 'chordal') {
  3454. this.options.catmullRom.alpha = 1.0;
  3455. }
  3456. else {
  3457. this.options.catmullRom.parametrization = 'centripetal';
  3458. this.options.catmullRom.alpha = 0.5;
  3459. }
  3460. }
  3461. }
  3462. }
  3463. if (this.yAxisLeft) {
  3464. if (options.dataAxis !== undefined) {
  3465. this.yAxisLeft.setOptions(this.options.dataAxis);
  3466. this.yAxisRight.setOptions(this.options.dataAxis);
  3467. }
  3468. }
  3469. if (this.legendLeft) {
  3470. if (options.legend !== undefined) {
  3471. this.legendLeft.setOptions(this.options.legend);
  3472. this.legendRight.setOptions(this.options.legend);
  3473. }
  3474. }
  3475. if (this.groups.hasOwnProperty(UNGROUPED)) {
  3476. this.groups[UNGROUPED].setOptions(options);
  3477. }
  3478. }
  3479. if (this.dom.frame) {
  3480. this._updateGraph();
  3481. }
  3482. };
  3483. /**
  3484. * Hide the component from the DOM
  3485. */
  3486. LineGraph.prototype.hide = function() {
  3487. // remove the frame containing the items
  3488. if (this.dom.frame.parentNode) {
  3489. this.dom.frame.parentNode.removeChild(this.dom.frame);
  3490. }
  3491. };
  3492. /**
  3493. * Show the component in the DOM (when not already visible).
  3494. * @return {Boolean} changed
  3495. */
  3496. LineGraph.prototype.show = function() {
  3497. // show frame containing the items
  3498. if (!this.dom.frame.parentNode) {
  3499. this.body.dom.center.appendChild(this.dom.frame);
  3500. }
  3501. };
  3502. /**
  3503. * Set items
  3504. * @param {vis.DataSet | null} items
  3505. */
  3506. LineGraph.prototype.setItems = function(items) {
  3507. var me = this,
  3508. ids,
  3509. oldItemsData = this.itemsData;
  3510. // replace the dataset
  3511. if (!items) {
  3512. this.itemsData = null;
  3513. }
  3514. else if (items instanceof DataSet || items instanceof DataView) {
  3515. this.itemsData = items;
  3516. }
  3517. else {
  3518. throw new TypeError('Data must be an instance of DataSet or DataView');
  3519. }
  3520. if (oldItemsData) {
  3521. // unsubscribe from old dataset
  3522. util.forEach(this.itemListeners, function (callback, event) {
  3523. oldItemsData.off(event, callback);
  3524. });
  3525. // remove all drawn items
  3526. ids = oldItemsData.getIds();
  3527. this._onRemove(ids);
  3528. }
  3529. if (this.itemsData) {
  3530. // subscribe to new dataset
  3531. var id = this.id;
  3532. util.forEach(this.itemListeners, function (callback, event) {
  3533. me.itemsData.on(event, callback, id);
  3534. });
  3535. // add all new items
  3536. ids = this.itemsData.getIds();
  3537. this._onAdd(ids);
  3538. }
  3539. this._updateUngrouped();
  3540. this._updateGraph();
  3541. this.redraw();
  3542. };
  3543. /**
  3544. * Set groups
  3545. * @param {vis.DataSet} groups
  3546. */
  3547. LineGraph.prototype.setGroups = function(groups) {
  3548. var me = this,
  3549. ids;
  3550. // unsubscribe from current dataset
  3551. if (this.groupsData) {
  3552. util.forEach(this.groupListeners, function (callback, event) {
  3553. me.groupsData.unsubscribe(event, callback);
  3554. });
  3555. // remove all drawn groups
  3556. ids = this.groupsData.getIds();
  3557. this.groupsData = null;
  3558. this._onRemoveGroups(ids); // note: this will cause a redraw
  3559. }
  3560. // replace the dataset
  3561. if (!groups) {
  3562. this.groupsData = null;
  3563. }
  3564. else if (groups instanceof DataSet || groups instanceof DataView) {
  3565. this.groupsData = groups;
  3566. }
  3567. else {
  3568. throw new TypeError('Data must be an instance of DataSet or DataView');
  3569. }
  3570. if (this.groupsData) {
  3571. // subscribe to new dataset
  3572. var id = this.id;
  3573. util.forEach(this.groupListeners, function (callback, event) {
  3574. me.groupsData.on(event, callback, id);
  3575. });
  3576. // draw all ms
  3577. ids = this.groupsData.getIds();
  3578. this._onAddGroups(ids);
  3579. }
  3580. this._onUpdate();
  3581. };
  3582. LineGraph.prototype._onUpdate = function(ids) {
  3583. this._updateUngrouped();
  3584. this._updateAllGroupData();
  3585. this._updateGraph();
  3586. this.redraw();
  3587. };
  3588. LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
  3589. LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
  3590. LineGraph.prototype._onUpdateGroups = function (groupIds) {
  3591. for (var i = 0; i < groupIds.length; i++) {
  3592. var group = this.groupsData.get(groupIds[i]);
  3593. this._updateGroup(group, groupIds[i]);
  3594. }
  3595. this._updateGraph();
  3596. this.redraw();
  3597. };
  3598. LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
  3599. LineGraph.prototype._onRemoveGroups = function (groupIds) {
  3600. for (var i = 0; i < groupIds.length; i++) {
  3601. if (!this.groups.hasOwnProperty(groupIds[i])) {
  3602. if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
  3603. this.yAxisRight.removeGroup(groupIds[i]);
  3604. this.legendRight.removeGroup(groupIds[i]);
  3605. this.legendRight.redraw();
  3606. }
  3607. else {
  3608. this.yAxisLeft.removeGroup(groupIds[i]);
  3609. this.legendLeft.removeGroup(groupIds[i]);
  3610. this.legendLeft.redraw();
  3611. }
  3612. delete this.groups[groupIds[i]];
  3613. }
  3614. }
  3615. this._updateUngrouped();
  3616. this._updateGraph();
  3617. this.redraw();
  3618. };
  3619. /**
  3620. * update a group object
  3621. *
  3622. * @param group
  3623. * @param groupId
  3624. * @private
  3625. */
  3626. LineGraph.prototype._updateGroup = function (group, groupId) {
  3627. if (!this.groups.hasOwnProperty(groupId)) {
  3628. this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
  3629. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  3630. this.yAxisRight.addGroup(groupId, this.groups[groupId]);
  3631. this.legendRight.addGroup(groupId, this.groups[groupId]);
  3632. }
  3633. else {
  3634. this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
  3635. this.legendLeft.addGroup(groupId, this.groups[groupId]);
  3636. }
  3637. }
  3638. else {
  3639. this.groups[groupId].update(group);
  3640. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  3641. this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
  3642. this.legendRight.updateGroup(groupId, this.groups[groupId]);
  3643. }
  3644. else {
  3645. this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
  3646. this.legendLeft.updateGroup(groupId, this.groups[groupId]);
  3647. }
  3648. }
  3649. this.legendLeft.redraw();
  3650. this.legendRight.redraw();
  3651. };
  3652. LineGraph.prototype._updateAllGroupData = function () {
  3653. if (this.itemsData != null) {
  3654. // ~450 ms @ 500k
  3655. var groupsContent = {};
  3656. for (var groupId in this.groups) {
  3657. if (this.groups.hasOwnProperty(groupId)) {
  3658. groupsContent[groupId] = [];
  3659. }
  3660. }
  3661. for (var itemId in this.itemsData._data) {
  3662. if (this.itemsData._data.hasOwnProperty(itemId)) {
  3663. var item = this.itemsData._data[itemId];
  3664. item.x = util.convert(item.x,"Date");
  3665. groupsContent[item.group].push(item);
  3666. }
  3667. }
  3668. for (var groupId in this.groups) {
  3669. if (this.groups.hasOwnProperty(groupId)) {
  3670. this.groups[groupId].setItems(groupsContent[groupId]);
  3671. }
  3672. }
  3673. // // ~4500ms @ 500k
  3674. // for (var groupId in this.groups) {
  3675. // if (this.groups.hasOwnProperty(groupId)) {
  3676. // this.groups[groupId].setItems(this.itemsData.get({filter:
  3677. // function (item) {
  3678. // return (item.group == groupId);
  3679. // }, type:{x:"Date"}}
  3680. // ));
  3681. // }
  3682. // }
  3683. }
  3684. };
  3685. /**
  3686. * Create or delete the group holding all ungrouped items. This group is used when
  3687. * there are no groups specified. This anonymous group is called 'graph'.
  3688. * @protected
  3689. */
  3690. LineGraph.prototype._updateUngrouped = function() {
  3691. if (this.itemsData != null) {
  3692. // var t0 = new Date();
  3693. var group = {id: UNGROUPED, content: this.options.defaultGroup};
  3694. this._updateGroup(group, UNGROUPED);
  3695. var ungroupedCounter = 0;
  3696. if (this.itemsData) {
  3697. for (var itemId in this.itemsData._data) {
  3698. if (this.itemsData._data.hasOwnProperty(itemId)) {
  3699. var item = this.itemsData._data[itemId];
  3700. if (item != undefined) {
  3701. if (item.hasOwnProperty('group')) {
  3702. if (item.group === undefined) {
  3703. item.group = UNGROUPED;
  3704. }
  3705. }
  3706. else {
  3707. item.group = UNGROUPED;
  3708. }
  3709. ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
  3710. }
  3711. }
  3712. }
  3713. }
  3714. // much much slower
  3715. // var datapoints = this.itemsData.get({
  3716. // filter: function (item) {return item.group === undefined;},
  3717. // showInternalIds:true
  3718. // });
  3719. // if (datapoints.length > 0) {
  3720. // var updateQuery = [];
  3721. // for (var i = 0; i < datapoints.length; i++) {
  3722. // updateQuery.push({id:datapoints[i].id, group: UNGROUPED});
  3723. // }
  3724. // this.itemsData.update(updateQuery, true);
  3725. // }
  3726. // var t1 = new Date();
  3727. // var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}});
  3728. if (ungroupedCounter == 0) {
  3729. delete this.groups[UNGROUPED];
  3730. this.legendLeft.removeGroup(UNGROUPED);
  3731. this.legendRight.removeGroup(UNGROUPED);
  3732. this.yAxisLeft.removeGroup(UNGROUPED);
  3733. this.yAxisRight.removeGroup(UNGROUPED);
  3734. }
  3735. // console.log("getting amount ungrouped",new Date() - t1);
  3736. // console.log("putting in ungrouped",new Date() - t0);
  3737. }
  3738. else {
  3739. delete this.groups[UNGROUPED];
  3740. this.legendLeft.removeGroup(UNGROUPED);
  3741. this.legendRight.removeGroup(UNGROUPED);
  3742. this.yAxisLeft.removeGroup(UNGROUPED);
  3743. this.yAxisRight.removeGroup(UNGROUPED);
  3744. }
  3745. this.legendLeft.redraw();
  3746. this.legendRight.redraw();
  3747. };
  3748. /**
  3749. * Redraw the component, mandatory function
  3750. * @return {boolean} Returns true if the component is resized
  3751. */
  3752. LineGraph.prototype.redraw = function() {
  3753. var resized = false;
  3754. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  3755. if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
  3756. resized = true;
  3757. }
  3758. // check if this component is resized
  3759. resized = this._isResized() || resized;
  3760. // check whether zoomed (in that case we need to re-stack everything)
  3761. var visibleInterval = this.body.range.end - this.body.range.start;
  3762. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  3763. this.lastVisibleInterval = visibleInterval;
  3764. this.lastWidth = this.width;
  3765. // calculate actual size and position
  3766. this.width = this.dom.frame.offsetWidth;
  3767. // the svg element is three times as big as the width, this allows for fully dragging left and right
  3768. // without reloading the graph. the controls for this are bound to events in the constructor
  3769. if (resized == true) {
  3770. this.svg.style.width = util.option.asSize(3*this.width);
  3771. this.svg.style.left = util.option.asSize(-this.width);
  3772. }
  3773. if (zoomed == true) {
  3774. this._updateGraph();
  3775. }
  3776. this.legendLeft.redraw();
  3777. this.legendRight.redraw();
  3778. return resized;
  3779. };
  3780. /**
  3781. * Update and redraw the graph.
  3782. *
  3783. */
  3784. LineGraph.prototype._updateGraph = function () {
  3785. // reset the svg elements
  3786. DOMutil.prepareElements(this.svgElements);
  3787. // // very slow...
  3788. // groupData = group.itemsData.get({filter:
  3789. // function (item) {
  3790. // return (item.x > minDate && item.x < maxDate);
  3791. // }}
  3792. // );
  3793. if (this.width != 0 && this.itemsData != null) {
  3794. var group, groupData, preprocessedGroup, i;
  3795. var preprocessedGroupData = [];
  3796. var processedGroupData = [];
  3797. var groupRanges = [];
  3798. var changeCalled = false;
  3799. // getting group Ids
  3800. var groupIds = [];
  3801. for (var groupId in this.groups) {
  3802. if (this.groups.hasOwnProperty(groupId)) {
  3803. groupIds.push(groupId);
  3804. }
  3805. }
  3806. // this is the range of the SVG canvas
  3807. var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
  3808. var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
  3809. // first select and preprocess the data from the datasets.
  3810. // the groups have their preselection of data, we now loop over this data to see
  3811. // what data we need to draw. Sorted data is much faster.
  3812. // more optimization is possible by doing the sampling before and using the binary search
  3813. // to find the end date to determine the increment.
  3814. if (groupIds.length > 0) {
  3815. for (i = 0; i < groupIds.length; i++) {
  3816. group = this.groups[groupIds[i]];
  3817. groupData = [];
  3818. // optimization for sorted data
  3819. if (group.options.sort == true) {
  3820. var guess = Math.max(0,util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
  3821. for (var j = guess; j < group.itemsData.length; j++) {
  3822. var item = group.itemsData[j];
  3823. if (item !== undefined) {
  3824. if (item.x > maxDate) {
  3825. groupData.push(item);
  3826. break;
  3827. }
  3828. else {
  3829. groupData.push(item);
  3830. }
  3831. }
  3832. }
  3833. }
  3834. else {
  3835. for (var j = 0; j < group.itemsData.length; j++) {
  3836. var item = group.itemsData[j];
  3837. if (item !== undefined) {
  3838. if (item.x > minDate && item.x < maxDate) {
  3839. groupData.push(item);
  3840. }
  3841. }
  3842. }
  3843. }
  3844. // preprocess, split into ranges and data
  3845. preprocessedGroup = this._preprocessData(groupData, group);
  3846. groupRanges.push({min: preprocessedGroup.min, max: preprocessedGroup.max});
  3847. preprocessedGroupData.push(preprocessedGroup.data);
  3848. }
  3849. // update the Y axis first, we use this data to draw at the correct Y points
  3850. // changeCalled is required to clean the SVG on a change emit.
  3851. changeCalled = this._updateYAxis(groupIds, groupRanges);
  3852. if (changeCalled == true) {
  3853. DOMutil.cleanupElements(this.svgElements);
  3854. this.body.emitter.emit("change");
  3855. return;
  3856. }
  3857. // with the yAxis scaled correctly, use this to get the Y values of the points.
  3858. for (i = 0; i < groupIds.length; i++) {
  3859. group = this.groups[groupIds[i]];
  3860. processedGroupData.push(this._convertYvalues(preprocessedGroupData[i],group))
  3861. }
  3862. // draw the groups
  3863. for (i = 0; i < groupIds.length; i++) {
  3864. group = this.groups[groupIds[i]];
  3865. if (group.options.style == 'line') {
  3866. this._drawLineGraph(processedGroupData[i], group);
  3867. }
  3868. else {
  3869. this._drawBarGraph (processedGroupData[i], group);
  3870. }
  3871. }
  3872. }
  3873. }
  3874. // cleanup unused svg elements
  3875. DOMutil.cleanupElements(this.svgElements);
  3876. };
  3877. /**
  3878. * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
  3879. * @param {array} groupIds
  3880. * @private
  3881. */
  3882. LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
  3883. var changeCalled = false;
  3884. var yAxisLeftUsed = false;
  3885. var yAxisRightUsed = false;
  3886. var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
  3887. var orientation = 'left';
  3888. // if groups are present
  3889. if (groupIds.length > 0) {
  3890. for (var i = 0; i < groupIds.length; i++) {
  3891. orientation = 'left';
  3892. var group = this.groups[groupIds[i]];
  3893. if (group.options.yAxisOrientation == 'right') {
  3894. orientation = 'right';
  3895. }
  3896. minVal = groupRanges[i].min;
  3897. maxVal = groupRanges[i].max;
  3898. if (orientation == 'left') {
  3899. yAxisLeftUsed = true;
  3900. minLeft = minLeft > minVal ? minVal : minLeft;
  3901. maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
  3902. }
  3903. else {
  3904. yAxisRightUsed = true;
  3905. minRight = minRight > minVal ? minVal : minRight;
  3906. maxRight = maxRight < maxVal ? maxVal : maxRight;
  3907. }
  3908. }
  3909. if (yAxisLeftUsed == true) {
  3910. this.yAxisLeft.setRange(minLeft, maxLeft);
  3911. }
  3912. if (yAxisRightUsed == true) {
  3913. this.yAxisRight.setRange(minRight, maxRight);
  3914. }
  3915. }
  3916. changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
  3917. changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
  3918. if (yAxisRightUsed == true && yAxisLeftUsed == true) {
  3919. this.yAxisLeft.drawIcons = true;
  3920. this.yAxisRight.drawIcons = true;
  3921. }
  3922. else {
  3923. this.yAxisLeft.drawIcons = false;
  3924. this.yAxisRight.drawIcons = false;
  3925. }
  3926. this.yAxisRight.master = !yAxisLeftUsed;
  3927. if (this.yAxisRight.master == false) {
  3928. if (yAxisRightUsed == true) {
  3929. this.yAxisLeft.lineOffset = this.yAxisRight.width;
  3930. }
  3931. changeCalled = this.yAxisLeft.redraw() || changeCalled;
  3932. this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
  3933. changeCalled = this.yAxisRight.redraw() || changeCalled;
  3934. }
  3935. else {
  3936. changeCalled = this.yAxisRight.redraw() || changeCalled;
  3937. }
  3938. return changeCalled;
  3939. };
  3940. /**
  3941. * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
  3942. *
  3943. * @param {boolean} axisUsed
  3944. * @returns {boolean}
  3945. * @private
  3946. * @param axis
  3947. */
  3948. LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
  3949. var changed = false;
  3950. if (axisUsed == false) {
  3951. if (axis.dom.frame.parentNode) {
  3952. axis.hide();
  3953. changed = true;
  3954. }
  3955. }
  3956. else {
  3957. if (!axis.dom.frame.parentNode) {
  3958. axis.show();
  3959. changed = true;
  3960. }
  3961. }
  3962. return changed;
  3963. };
  3964. /**
  3965. * draw a bar graph
  3966. * @param datapoints
  3967. * @param group
  3968. */
  3969. LineGraph.prototype._drawBarGraph = function (dataset, group) {
  3970. if (dataset != null) {
  3971. if (dataset.length > 0) {
  3972. var coreDistance;
  3973. var minWidth = 0.1 * group.options.barChart.width;
  3974. var offset = 0;
  3975. var width = group.options.barChart.width;
  3976. if (group.options.barChart.align == 'left') {offset -= 0.5*width;}
  3977. else if (group.options.barChart.align == 'right') {offset += 0.5*width;}
  3978. for (var i = 0; i < dataset.length; i++) {
  3979. // dynammically downscale the width so there is no overlap up to 1/10th the original width
  3980. if (i+1 < dataset.length) {coreDistance = Math.abs(dataset[i+1].x - dataset[i].x);}
  3981. if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(dataset[i-1].x - dataset[i].x));}
  3982. if (coreDistance < width) {width = coreDistance < minWidth ? minWidth : coreDistance;}
  3983. DOMutil.drawBar(dataset[i].x + offset, dataset[i].y, width, group.zeroPosition - dataset[i].y, group.className + ' bar', this.svgElements, this.svg);
  3984. }
  3985. // draw points
  3986. if (group.options.drawPoints.enabled == true) {
  3987. this._drawPoints(dataset, group, this.svgElements, this.svg, offset);
  3988. }
  3989. }
  3990. }
  3991. };
  3992. /**
  3993. * draw a line graph
  3994. *
  3995. * @param datapoints
  3996. * @param group
  3997. */
  3998. LineGraph.prototype._drawLineGraph = function (dataset, group) {
  3999. if (dataset != null) {
  4000. if (dataset.length > 0) {
  4001. var path, d;
  4002. var svgHeight = Number(this.svg.style.height.replace("px",""));
  4003. path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
  4004. path.setAttributeNS(null, "class", group.className);
  4005. // construct path from dataset
  4006. if (group.options.catmullRom.enabled == true) {
  4007. d = this._catmullRom(dataset, group);
  4008. }
  4009. else {
  4010. d = this._linear(dataset);
  4011. }
  4012. // append with points for fill and finalize the path
  4013. if (group.options.shaded.enabled == true) {
  4014. var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
  4015. var dFill;
  4016. if (group.options.shaded.orientation == 'top') {
  4017. dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
  4018. }
  4019. else {
  4020. dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
  4021. }
  4022. fillPath.setAttributeNS(null, "class", group.className + " fill");
  4023. fillPath.setAttributeNS(null, "d", dFill);
  4024. }
  4025. // copy properties to path for drawing.
  4026. path.setAttributeNS(null, "d", "M" + d);
  4027. // draw points
  4028. if (group.options.drawPoints.enabled == true) {
  4029. this._drawPoints(dataset, group, this.svgElements, this.svg);
  4030. }
  4031. }
  4032. }
  4033. };
  4034. /**
  4035. * draw the data points
  4036. *
  4037. * @param dataset
  4038. * @param JSONcontainer
  4039. * @param svg
  4040. * @param group
  4041. */
  4042. LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
  4043. if (offset === undefined) {offset = 0;}
  4044. for (var i = 0; i < dataset.length; i++) {
  4045. DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
  4046. }
  4047. };
  4048. /**
  4049. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  4050. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  4051. * the yAxis.
  4052. *
  4053. * @param datapoints
  4054. * @returns {Array}
  4055. * @private
  4056. */
  4057. LineGraph.prototype._preprocessData = function (datapoints, group) {
  4058. var extractedData = [];
  4059. var xValue, yValue;
  4060. var toScreen = this.body.util.toScreen;
  4061. var increment = 1;
  4062. var amountOfPoints = datapoints.length;
  4063. var yMin = datapoints[0].y;
  4064. var yMax = datapoints[0].y;
  4065. // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
  4066. // of width changing of the yAxis.
  4067. if (group.options.sampling == true) {
  4068. var xDistance = this.body.util.toGlobalScreen(datapoints[datapoints.length-1].x) - this.body.util.toGlobalScreen(datapoints[0].x);
  4069. var pointsPerPixel = amountOfPoints/xDistance;
  4070. increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1,Math.round(pointsPerPixel)));
  4071. }
  4072. for (var i = 0; i < amountOfPoints; i += increment) {
  4073. xValue = toScreen(datapoints[i].x) + this.width - 1;
  4074. yValue = datapoints[i].y;
  4075. extractedData.push({x: xValue, y: yValue});
  4076. yMin = yMin > yValue ? yValue : yMin;
  4077. yMax = yMax < yValue ? yValue : yMax;
  4078. }
  4079. // extractedData.sort(function (a,b) {return a.x - b.x;});
  4080. return {min: yMin, max: yMax, data: extractedData};
  4081. };
  4082. /**
  4083. * This uses the DataAxis object to generate the correct Y coordinate on the SVG window. It uses the
  4084. * util function toScreen to get the x coordinate from the timestamp.
  4085. *
  4086. * @param datapoints
  4087. * @param options
  4088. * @returns {Array}
  4089. * @private
  4090. */
  4091. LineGraph.prototype._convertYvalues = function (datapoints, group) {
  4092. var extractedData = [];
  4093. var xValue, yValue;
  4094. var axis = this.yAxisLeft;
  4095. var svgHeight = Number(this.svg.style.height.replace("px",""));
  4096. if (group.options.yAxisOrientation == 'right') {
  4097. axis = this.yAxisRight;
  4098. }
  4099. for (var i = 0; i < datapoints.length; i++) {
  4100. xValue = datapoints[i].x;
  4101. yValue = Math.round(axis.convertValue(datapoints[i].y));
  4102. extractedData.push({x: xValue, y: yValue});
  4103. }
  4104. group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
  4105. // extractedData.sort(function (a,b) {return a.x - b.x;});
  4106. return extractedData;
  4107. };
  4108. /**
  4109. * This uses an uniform parametrization of the CatmullRom algorithm:
  4110. * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
  4111. * @param data
  4112. * @returns {string}
  4113. * @private
  4114. */
  4115. LineGraph.prototype._catmullRomUniform = function(data) {
  4116. // catmull rom
  4117. var p0, p1, p2, p3, bp1, bp2;
  4118. var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  4119. var normalization = 1/6;
  4120. var length = data.length;
  4121. for (var i = 0; i < length - 1; i++) {
  4122. p0 = (i == 0) ? data[0] : data[i-1];
  4123. p1 = data[i];
  4124. p2 = data[i+1];
  4125. p3 = (i + 2 < length) ? data[i+2] : p2;
  4126. // Catmull-Rom to Cubic Bezier conversion matrix
  4127. // 0 1 0 0
  4128. // -1/6 1 1/6 0
  4129. // 0 1/6 1 -1/6
  4130. // 0 0 1 0
  4131. // bp0 = { x: p1.x, y: p1.y };
  4132. bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
  4133. bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
  4134. // bp0 = { x: p2.x, y: p2.y };
  4135. d += "C" +
  4136. bp1.x + "," +
  4137. bp1.y + " " +
  4138. bp2.x + "," +
  4139. bp2.y + " " +
  4140. p2.x + "," +
  4141. p2.y + " ";
  4142. }
  4143. return d;
  4144. };
  4145. /**
  4146. * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
  4147. * By default, the centripetal parameterization is used because this gives the nicest results.
  4148. * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
  4149. *
  4150. * One optimization can be used to reuse distances since this is a sliding window approach.
  4151. * @param data
  4152. * @returns {string}
  4153. * @private
  4154. */
  4155. LineGraph.prototype._catmullRom = function(data, group) {
  4156. var alpha = group.options.catmullRom.alpha;
  4157. if (alpha == 0 || alpha === undefined) {
  4158. return this._catmullRomUniform(data);
  4159. }
  4160. else {
  4161. var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
  4162. var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
  4163. var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  4164. var length = data.length;
  4165. for (var i = 0; i < length - 1; i++) {
  4166. p0 = (i == 0) ? data[0] : data[i-1];
  4167. p1 = data[i];
  4168. p2 = data[i+1];
  4169. p3 = (i + 2 < length) ? data[i+2] : p2;
  4170. d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
  4171. d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
  4172. d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
  4173. // Catmull-Rom to Cubic Bezier conversion matrix
  4174. //
  4175. // A = 2d1^2a + 3d1^a * d2^a + d3^2a
  4176. // B = 2d3^2a + 3d3^a * d2^a + d2^2a
  4177. //
  4178. // [ 0 1 0 0 ]
  4179. // [ -d2^2a/N A/N d1^2a/N 0 ]
  4180. // [ 0 d3^2a/M B/M -d2^2a/M ]
  4181. // [ 0 0 1 0 ]
  4182. // [ 0 1 0 0 ]
  4183. // [ -d2pow2a/N A/N d1pow2a/N 0 ]
  4184. // [ 0 d3pow2a/M B/M -d2pow2a/M ]
  4185. // [ 0 0 1 0 ]
  4186. d3powA = Math.pow(d3, alpha);
  4187. d3pow2A = Math.pow(d3,2*alpha);
  4188. d2powA = Math.pow(d2, alpha);
  4189. d2pow2A = Math.pow(d2,2*alpha);
  4190. d1powA = Math.pow(d1, alpha);
  4191. d1pow2A = Math.pow(d1,2*alpha);
  4192. A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
  4193. B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
  4194. N = 3*d1powA * (d1powA + d2powA);
  4195. if (N > 0) {N = 1 / N;}
  4196. M = 3*d3powA * (d3powA + d2powA);
  4197. if (M > 0) {M = 1 / M;}
  4198. bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
  4199. y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
  4200. bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
  4201. y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
  4202. if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
  4203. if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
  4204. d += "C" +
  4205. bp1.x + "," +
  4206. bp1.y + " " +
  4207. bp2.x + "," +
  4208. bp2.y + " " +
  4209. p2.x + "," +
  4210. p2.y + " ";
  4211. }
  4212. return d;
  4213. }
  4214. };
  4215. /**
  4216. * this generates the SVG path for a linear drawing between datapoints.
  4217. * @param data
  4218. * @returns {string}
  4219. * @private
  4220. */
  4221. LineGraph.prototype._linear = function(data) {
  4222. // linear
  4223. var d = "";
  4224. for (var i = 0; i < data.length; i++) {
  4225. if (i == 0) {
  4226. d += data[i].x + "," + data[i].y;
  4227. }
  4228. else {
  4229. d += " " + data[i].x + "," + data[i].y;
  4230. }
  4231. }
  4232. return d;
  4233. };
  4234. /**
  4235. * @constructor DataStep
  4236. * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
  4237. * end data point. The class itself determines the best scale (step size) based on the
  4238. * provided start Date, end Date, and minimumStep.
  4239. *
  4240. * If minimumStep is provided, the step size is chosen as close as possible
  4241. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4242. * provided, the scale is set to 1 DAY.
  4243. * The minimumStep should correspond with the onscreen size of about 6 characters
  4244. *
  4245. * Alternatively, you can set a scale by hand.
  4246. * After creation, you can initialize the class by executing first(). Then you
  4247. * can iterate from the start date to the end date via next(). You can check if
  4248. * the end date is reached with the function hasNext(). After each step, you can
  4249. * retrieve the current date via getCurrent().
  4250. * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
  4251. * days, to years.
  4252. *
  4253. * Version: 1.2
  4254. *
  4255. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  4256. * or new Date(2010, 9, 21, 23, 45, 00)
  4257. * @param {Date} [end] The end date
  4258. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4259. */
  4260. function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
  4261. // variables
  4262. this.current = 0;
  4263. this.autoScale = true;
  4264. this.stepIndex = 0;
  4265. this.step = 1;
  4266. this.scale = 1;
  4267. this.marginStart;
  4268. this.marginEnd;
  4269. this.majorSteps = [1, 2, 5, 10];
  4270. this.minorSteps = [0.25, 0.5, 1, 2];
  4271. this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
  4272. }
  4273. /**
  4274. * Set a new range
  4275. * If minimumStep is provided, the step size is chosen as close as possible
  4276. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4277. * provided, the scale is set to 1 DAY.
  4278. * The minimumStep should correspond with the onscreen size of about 6 characters
  4279. * @param {Number} [start] The start date and time.
  4280. * @param {Number} [end] The end date and time.
  4281. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4282. */
  4283. DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
  4284. this._start = start;
  4285. this._end = end;
  4286. if (this.autoScale) {
  4287. this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
  4288. }
  4289. this.setFirst();
  4290. };
  4291. /**
  4292. * Automatically determine the scale that bests fits the provided minimum step
  4293. * @param {Number} [minimumStep] The minimum step size in milliseconds
  4294. */
  4295. DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
  4296. // round to floor
  4297. var size = this._end - this._start;
  4298. var safeSize = size * 1.1;
  4299. var minimumStepValue = minimumStep * (safeSize / containerHeight);
  4300. var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
  4301. var minorStepIdx = -1;
  4302. var magnitudefactor = Math.pow(10,orderOfMagnitude);
  4303. var start = 0;
  4304. if (orderOfMagnitude < 0) {
  4305. start = orderOfMagnitude;
  4306. }
  4307. var solutionFound = false;
  4308. for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
  4309. magnitudefactor = Math.pow(10,i);
  4310. for (var j = 0; j < this.minorSteps.length; j++) {
  4311. var stepSize = magnitudefactor * this.minorSteps[j];
  4312. if (stepSize >= minimumStepValue) {
  4313. solutionFound = true;
  4314. minorStepIdx = j;
  4315. break;
  4316. }
  4317. }
  4318. if (solutionFound == true) {
  4319. break;
  4320. }
  4321. }
  4322. this.stepIndex = minorStepIdx;
  4323. this.scale = magnitudefactor;
  4324. this.step = magnitudefactor * this.minorSteps[minorStepIdx];
  4325. };
  4326. /**
  4327. * Set the range iterator to the start date.
  4328. */
  4329. DataStep.prototype.first = function() {
  4330. this.setFirst();
  4331. };
  4332. /**
  4333. * Round the current date to the first minor date value
  4334. * This must be executed once when the current date is set to start Date
  4335. */
  4336. DataStep.prototype.setFirst = function() {
  4337. var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
  4338. var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
  4339. this.marginEnd = this.roundToMinor(niceEnd);
  4340. this.marginStart = this.roundToMinor(niceStart);
  4341. this.marginRange = this.marginEnd - this.marginStart;
  4342. this.current = this.marginEnd;
  4343. };
  4344. DataStep.prototype.roundToMinor = function(value) {
  4345. var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
  4346. if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
  4347. return rounded + (this.scale * this.minorSteps[this.stepIndex]);
  4348. }
  4349. else {
  4350. return rounded;
  4351. }
  4352. }
  4353. /**
  4354. * Check if the there is a next step
  4355. * @return {boolean} true if the current date has not passed the end date
  4356. */
  4357. DataStep.prototype.hasNext = function () {
  4358. return (this.current >= this.marginStart);
  4359. };
  4360. /**
  4361. * Do the next step
  4362. */
  4363. DataStep.prototype.next = function() {
  4364. var prev = this.current;
  4365. this.current -= this.step;
  4366. // safety mechanism: if current time is still unchanged, move to the end
  4367. if (this.current == prev) {
  4368. this.current = this._end;
  4369. }
  4370. };
  4371. /**
  4372. * Do the next step
  4373. */
  4374. DataStep.prototype.previous = function() {
  4375. this.current += this.step;
  4376. this.marginEnd += this.step;
  4377. this.marginRange = this.marginEnd - this.marginStart;
  4378. };
  4379. /**
  4380. * Get the current datetime
  4381. * @return {Number} current The current date
  4382. */
  4383. DataStep.prototype.getCurrent = function() {
  4384. var toPrecision = '' + Number(this.current).toPrecision(5);
  4385. for (var i = toPrecision.length-1; i > 0; i--) {
  4386. if (toPrecision[i] == "0") {
  4387. toPrecision = toPrecision.slice(0,i);
  4388. }
  4389. else if (toPrecision[i] == "." || toPrecision[i] == ",") {
  4390. toPrecision = toPrecision.slice(0,i);
  4391. break;
  4392. }
  4393. else{
  4394. break;
  4395. }
  4396. }
  4397. return toPrecision;
  4398. };
  4399. /**
  4400. * Snap a date to a rounded value.
  4401. * The snap intervals are dependent on the current scale and step.
  4402. * @param {Date} date the date to be snapped.
  4403. * @return {Date} snappedDate
  4404. */
  4405. DataStep.prototype.snap = function(date) {
  4406. };
  4407. /**
  4408. * Check if the current value is a major value (for example when the step
  4409. * is DAY, a major value is each first day of the MONTH)
  4410. * @return {boolean} true if current date is major, else false.
  4411. */
  4412. DataStep.prototype.isMajor = function() {
  4413. return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
  4414. };
  4415. /**
  4416. * Utility functions for ordering and stacking of items
  4417. */
  4418. var stack = {};
  4419. /**
  4420. * Order items by their start data
  4421. * @param {Item[]} items
  4422. */
  4423. stack.orderByStart = function(items) {
  4424. items.sort(function (a, b) {
  4425. return a.data.start - b.data.start;
  4426. });
  4427. };
  4428. /**
  4429. * Order items by their end date. If they have no end date, their start date
  4430. * is used.
  4431. * @param {Item[]} items
  4432. */
  4433. stack.orderByEnd = function(items) {
  4434. items.sort(function (a, b) {
  4435. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  4436. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  4437. return aTime - bTime;
  4438. });
  4439. };
  4440. /**
  4441. * Adjust vertical positions of the items such that they don't overlap each
  4442. * other.
  4443. * @param {Item[]} items
  4444. * All visible items
  4445. * @param {{item: number, axis: number}} margin
  4446. * Margins between items and between items and the axis.
  4447. * @param {boolean} [force=false]
  4448. * If true, all items will be repositioned. If false (default), only
  4449. * items having a top===null will be re-stacked
  4450. */
  4451. stack.stack = function(items, margin, force) {
  4452. var i, iMax;
  4453. if (force) {
  4454. // reset top position of all items
  4455. for (i = 0, iMax = items.length; i < iMax; i++) {
  4456. items[i].top = null;
  4457. }
  4458. }
  4459. // calculate new, non-overlapping positions
  4460. for (i = 0, iMax = items.length; i < iMax; i++) {
  4461. var item = items[i];
  4462. if (item.top === null) {
  4463. // initialize top position
  4464. item.top = margin.axis;
  4465. do {
  4466. // TODO: optimize checking for overlap. when there is a gap without items,
  4467. // you only need to check for items from the next item on, not from zero
  4468. var collidingItem = null;
  4469. for (var j = 0, jj = items.length; j < jj; j++) {
  4470. var other = items[j];
  4471. if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
  4472. collidingItem = other;
  4473. break;
  4474. }
  4475. }
  4476. if (collidingItem != null) {
  4477. // There is a collision. Reposition the items above the colliding element
  4478. item.top = collidingItem.top + collidingItem.height + margin.item;
  4479. }
  4480. } while (collidingItem);
  4481. }
  4482. }
  4483. };
  4484. /**
  4485. * Adjust vertical positions of the items without stacking them
  4486. * @param {Item[]} items
  4487. * All visible items
  4488. * @param {{item: number, axis: number}} margin
  4489. * Margins between items and between items and the axis.
  4490. */
  4491. stack.nostack = function(items, margin) {
  4492. var i, iMax;
  4493. // reset top position of all items
  4494. for (i = 0, iMax = items.length; i < iMax; i++) {
  4495. items[i].top = margin.axis;
  4496. }
  4497. };
  4498. /**
  4499. * Test if the two provided items collide
  4500. * The items must have parameters left, width, top, and height.
  4501. * @param {Item} a The first item
  4502. * @param {Item} b The second item
  4503. * @param {Number} margin A minimum required margin.
  4504. * If margin is provided, the two items will be
  4505. * marked colliding when they overlap or
  4506. * when the margin between the two is smaller than
  4507. * the requested margin.
  4508. * @return {boolean} true if a and b collide, else false
  4509. */
  4510. stack.collision = function(a, b, margin) {
  4511. return ((a.left - margin) < (b.left + b.width) &&
  4512. (a.left + a.width + margin) > b.left &&
  4513. (a.top - margin) < (b.top + b.height) &&
  4514. (a.top + a.height + margin) > b.top);
  4515. };
  4516. /**
  4517. * @constructor TimeStep
  4518. * The class TimeStep is an iterator for dates. You provide a start date and an
  4519. * end date. The class itself determines the best scale (step size) based on the
  4520. * provided start Date, end Date, and minimumStep.
  4521. *
  4522. * If minimumStep is provided, the step size is chosen as close as possible
  4523. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4524. * provided, the scale is set to 1 DAY.
  4525. * The minimumStep should correspond with the onscreen size of about 6 characters
  4526. *
  4527. * Alternatively, you can set a scale by hand.
  4528. * After creation, you can initialize the class by executing first(). Then you
  4529. * can iterate from the start date to the end date via next(). You can check if
  4530. * the end date is reached with the function hasNext(). After each step, you can
  4531. * retrieve the current date via getCurrent().
  4532. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  4533. * days, to years.
  4534. *
  4535. * Version: 1.2
  4536. *
  4537. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  4538. * or new Date(2010, 9, 21, 23, 45, 00)
  4539. * @param {Date} [end] The end date
  4540. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4541. */
  4542. function TimeStep(start, end, minimumStep) {
  4543. // variables
  4544. this.current = new Date();
  4545. this._start = new Date();
  4546. this._end = new Date();
  4547. this.autoScale = true;
  4548. this.scale = TimeStep.SCALE.DAY;
  4549. this.step = 1;
  4550. // initialize the range
  4551. this.setRange(start, end, minimumStep);
  4552. }
  4553. /// enum scale
  4554. TimeStep.SCALE = {
  4555. MILLISECOND: 1,
  4556. SECOND: 2,
  4557. MINUTE: 3,
  4558. HOUR: 4,
  4559. DAY: 5,
  4560. WEEKDAY: 6,
  4561. MONTH: 7,
  4562. YEAR: 8
  4563. };
  4564. /**
  4565. * Set a new range
  4566. * If minimumStep is provided, the step size is chosen as close as possible
  4567. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4568. * provided, the scale is set to 1 DAY.
  4569. * The minimumStep should correspond with the onscreen size of about 6 characters
  4570. * @param {Date} [start] The start date and time.
  4571. * @param {Date} [end] The end date and time.
  4572. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  4573. */
  4574. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  4575. if (!(start instanceof Date) || !(end instanceof Date)) {
  4576. throw "No legal start or end date in method setRange";
  4577. }
  4578. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  4579. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  4580. if (this.autoScale) {
  4581. this.setMinimumStep(minimumStep);
  4582. }
  4583. };
  4584. /**
  4585. * Set the range iterator to the start date.
  4586. */
  4587. TimeStep.prototype.first = function() {
  4588. this.current = new Date(this._start.valueOf());
  4589. this.roundToMinor();
  4590. };
  4591. /**
  4592. * Round the current date to the first minor date value
  4593. * This must be executed once when the current date is set to start Date
  4594. */
  4595. TimeStep.prototype.roundToMinor = function() {
  4596. // round to floor
  4597. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  4598. //noinspection FallthroughInSwitchStatementJS
  4599. switch (this.scale) {
  4600. case TimeStep.SCALE.YEAR:
  4601. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  4602. this.current.setMonth(0);
  4603. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  4604. case TimeStep.SCALE.DAY: // intentional fall through
  4605. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  4606. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  4607. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  4608. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  4609. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  4610. }
  4611. if (this.step != 1) {
  4612. // round down to the first minor value that is a multiple of the current step size
  4613. switch (this.scale) {
  4614. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  4615. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  4616. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  4617. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  4618. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4619. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  4620. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  4621. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  4622. default: break;
  4623. }
  4624. }
  4625. };
  4626. /**
  4627. * Check if the there is a next step
  4628. * @return {boolean} true if the current date has not passed the end date
  4629. */
  4630. TimeStep.prototype.hasNext = function () {
  4631. return (this.current.valueOf() <= this._end.valueOf());
  4632. };
  4633. /**
  4634. * Do the next step
  4635. */
  4636. TimeStep.prototype.next = function() {
  4637. var prev = this.current.valueOf();
  4638. // Two cases, needed to prevent issues with switching daylight savings
  4639. // (end of March and end of October)
  4640. if (this.current.getMonth() < 6) {
  4641. switch (this.scale) {
  4642. case TimeStep.SCALE.MILLISECOND:
  4643. this.current = new Date(this.current.valueOf() + this.step); break;
  4644. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  4645. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  4646. case TimeStep.SCALE.HOUR:
  4647. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  4648. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  4649. var h = this.current.getHours();
  4650. this.current.setHours(h - (h % this.step));
  4651. break;
  4652. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4653. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  4654. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  4655. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  4656. default: break;
  4657. }
  4658. }
  4659. else {
  4660. switch (this.scale) {
  4661. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  4662. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  4663. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  4664. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  4665. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4666. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  4667. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  4668. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  4669. default: break;
  4670. }
  4671. }
  4672. if (this.step != 1) {
  4673. // round down to the correct major value
  4674. switch (this.scale) {
  4675. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  4676. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  4677. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  4678. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  4679. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4680. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  4681. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  4682. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  4683. default: break;
  4684. }
  4685. }
  4686. // safety mechanism: if current time is still unchanged, move to the end
  4687. if (this.current.valueOf() == prev) {
  4688. this.current = new Date(this._end.valueOf());
  4689. }
  4690. };
  4691. /**
  4692. * Get the current datetime
  4693. * @return {Date} current The current date
  4694. */
  4695. TimeStep.prototype.getCurrent = function() {
  4696. return this.current;
  4697. };
  4698. /**
  4699. * Set a custom scale. Autoscaling will be disabled.
  4700. * For example setScale(SCALE.MINUTES, 5) will result
  4701. * in minor steps of 5 minutes, and major steps of an hour.
  4702. *
  4703. * @param {TimeStep.SCALE} newScale
  4704. * A scale. Choose from SCALE.MILLISECOND,
  4705. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  4706. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  4707. * SCALE.YEAR.
  4708. * @param {Number} newStep A step size, by default 1. Choose for
  4709. * example 1, 2, 5, or 10.
  4710. */
  4711. TimeStep.prototype.setScale = function(newScale, newStep) {
  4712. this.scale = newScale;
  4713. if (newStep > 0) {
  4714. this.step = newStep;
  4715. }
  4716. this.autoScale = false;
  4717. };
  4718. /**
  4719. * Enable or disable autoscaling
  4720. * @param {boolean} enable If true, autoascaling is set true
  4721. */
  4722. TimeStep.prototype.setAutoScale = function (enable) {
  4723. this.autoScale = enable;
  4724. };
  4725. /**
  4726. * Automatically determine the scale that bests fits the provided minimum step
  4727. * @param {Number} [minimumStep] The minimum step size in milliseconds
  4728. */
  4729. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  4730. if (minimumStep == undefined) {
  4731. return;
  4732. }
  4733. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  4734. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  4735. var stepDay = (1000 * 60 * 60 * 24);
  4736. var stepHour = (1000 * 60 * 60);
  4737. var stepMinute = (1000 * 60);
  4738. var stepSecond = (1000);
  4739. var stepMillisecond= (1);
  4740. // find the smallest step that is larger than the provided minimumStep
  4741. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  4742. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  4743. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  4744. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  4745. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  4746. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  4747. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  4748. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  4749. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  4750. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  4751. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  4752. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  4753. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  4754. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  4755. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  4756. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  4757. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  4758. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  4759. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  4760. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  4761. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  4762. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  4763. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  4764. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  4765. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  4766. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  4767. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  4768. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  4769. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  4770. };
  4771. /**
  4772. * Snap a date to a rounded value.
  4773. * The snap intervals are dependent on the current scale and step.
  4774. * @param {Date} date the date to be snapped.
  4775. * @return {Date} snappedDate
  4776. */
  4777. TimeStep.prototype.snap = function(date) {
  4778. var clone = new Date(date.valueOf());
  4779. if (this.scale == TimeStep.SCALE.YEAR) {
  4780. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  4781. clone.setFullYear(Math.round(year / this.step) * this.step);
  4782. clone.setMonth(0);
  4783. clone.setDate(0);
  4784. clone.setHours(0);
  4785. clone.setMinutes(0);
  4786. clone.setSeconds(0);
  4787. clone.setMilliseconds(0);
  4788. }
  4789. else if (this.scale == TimeStep.SCALE.MONTH) {
  4790. if (clone.getDate() > 15) {
  4791. clone.setDate(1);
  4792. clone.setMonth(clone.getMonth() + 1);
  4793. // important: first set Date to 1, after that change the month.
  4794. }
  4795. else {
  4796. clone.setDate(1);
  4797. }
  4798. clone.setHours(0);
  4799. clone.setMinutes(0);
  4800. clone.setSeconds(0);
  4801. clone.setMilliseconds(0);
  4802. }
  4803. else if (this.scale == TimeStep.SCALE.DAY) {
  4804. //noinspection FallthroughInSwitchStatementJS
  4805. switch (this.step) {
  4806. case 5:
  4807. case 2:
  4808. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  4809. default:
  4810. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  4811. }
  4812. clone.setMinutes(0);
  4813. clone.setSeconds(0);
  4814. clone.setMilliseconds(0);
  4815. }
  4816. else if (this.scale == TimeStep.SCALE.WEEKDAY) {
  4817. //noinspection FallthroughInSwitchStatementJS
  4818. switch (this.step) {
  4819. case 5:
  4820. case 2:
  4821. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  4822. default:
  4823. clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
  4824. }
  4825. clone.setMinutes(0);
  4826. clone.setSeconds(0);
  4827. clone.setMilliseconds(0);
  4828. }
  4829. else if (this.scale == TimeStep.SCALE.HOUR) {
  4830. switch (this.step) {
  4831. case 4:
  4832. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  4833. default:
  4834. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  4835. }
  4836. clone.setSeconds(0);
  4837. clone.setMilliseconds(0);
  4838. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  4839. //noinspection FallthroughInSwitchStatementJS
  4840. switch (this.step) {
  4841. case 15:
  4842. case 10:
  4843. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  4844. clone.setSeconds(0);
  4845. break;
  4846. case 5:
  4847. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  4848. default:
  4849. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  4850. }
  4851. clone.setMilliseconds(0);
  4852. }
  4853. else if (this.scale == TimeStep.SCALE.SECOND) {
  4854. //noinspection FallthroughInSwitchStatementJS
  4855. switch (this.step) {
  4856. case 15:
  4857. case 10:
  4858. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  4859. clone.setMilliseconds(0);
  4860. break;
  4861. case 5:
  4862. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  4863. default:
  4864. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  4865. }
  4866. }
  4867. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  4868. var step = this.step > 5 ? this.step / 2 : 1;
  4869. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  4870. }
  4871. return clone;
  4872. };
  4873. /**
  4874. * Check if the current value is a major value (for example when the step
  4875. * is DAY, a major value is each first day of the MONTH)
  4876. * @return {boolean} true if current date is major, else false.
  4877. */
  4878. TimeStep.prototype.isMajor = function() {
  4879. switch (this.scale) {
  4880. case TimeStep.SCALE.MILLISECOND:
  4881. return (this.current.getMilliseconds() == 0);
  4882. case TimeStep.SCALE.SECOND:
  4883. return (this.current.getSeconds() == 0);
  4884. case TimeStep.SCALE.MINUTE:
  4885. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  4886. // Note: this is no bug. Major label is equal for both minute and hour scale
  4887. case TimeStep.SCALE.HOUR:
  4888. return (this.current.getHours() == 0);
  4889. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4890. case TimeStep.SCALE.DAY:
  4891. return (this.current.getDate() == 1);
  4892. case TimeStep.SCALE.MONTH:
  4893. return (this.current.getMonth() == 0);
  4894. case TimeStep.SCALE.YEAR:
  4895. return false;
  4896. default:
  4897. return false;
  4898. }
  4899. };
  4900. /**
  4901. * Returns formatted text for the minor axislabel, depending on the current
  4902. * date and the scale. For example when scale is MINUTE, the current time is
  4903. * formatted as "hh:mm".
  4904. * @param {Date} [date] custom date. if not provided, current date is taken
  4905. */
  4906. TimeStep.prototype.getLabelMinor = function(date) {
  4907. if (date == undefined) {
  4908. date = this.current;
  4909. }
  4910. switch (this.scale) {
  4911. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  4912. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  4913. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  4914. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  4915. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  4916. case TimeStep.SCALE.DAY: return moment(date).format('D');
  4917. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  4918. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  4919. default: return '';
  4920. }
  4921. };
  4922. /**
  4923. * Returns formatted text for the major axis label, depending on the current
  4924. * date and the scale. For example when scale is MINUTE, the major scale is
  4925. * hours, and the hour will be formatted as "hh".
  4926. * @param {Date} [date] custom date. if not provided, current date is taken
  4927. */
  4928. TimeStep.prototype.getLabelMajor = function(date) {
  4929. if (date == undefined) {
  4930. date = this.current;
  4931. }
  4932. //noinspection FallthroughInSwitchStatementJS
  4933. switch (this.scale) {
  4934. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  4935. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  4936. case TimeStep.SCALE.MINUTE:
  4937. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  4938. case TimeStep.SCALE.WEEKDAY:
  4939. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  4940. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  4941. case TimeStep.SCALE.YEAR: return '';
  4942. default: return '';
  4943. }
  4944. };
  4945. /**
  4946. * @constructor Range
  4947. * A Range controls a numeric range with a start and end value.
  4948. * The Range adjusts the range based on mouse events or programmatic changes,
  4949. * and triggers events when the range is changing or has been changed.
  4950. * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
  4951. * @param {Object} [options] See description at Range.setOptions
  4952. */
  4953. function Range(body, options) {
  4954. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  4955. this.start = now.clone().add('days', -3).valueOf(); // Number
  4956. this.end = now.clone().add('days', 4).valueOf(); // Number
  4957. this.body = body;
  4958. // default options
  4959. this.defaultOptions = {
  4960. start: null,
  4961. end: null,
  4962. direction: 'horizontal', // 'horizontal' or 'vertical'
  4963. moveable: true,
  4964. zoomable: true,
  4965. min: null,
  4966. max: null,
  4967. zoomMin: 10, // milliseconds
  4968. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
  4969. };
  4970. this.options = util.extend({}, this.defaultOptions);
  4971. this.props = {
  4972. touch: {}
  4973. };
  4974. // drag listeners for dragging
  4975. this.body.emitter.on('dragstart', this._onDragStart.bind(this));
  4976. this.body.emitter.on('drag', this._onDrag.bind(this));
  4977. this.body.emitter.on('dragend', this._onDragEnd.bind(this));
  4978. // ignore dragging when holding
  4979. this.body.emitter.on('hold', this._onHold.bind(this));
  4980. // mouse wheel for zooming
  4981. this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
  4982. this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  4983. // pinch to zoom
  4984. this.body.emitter.on('touch', this._onTouch.bind(this));
  4985. this.body.emitter.on('pinch', this._onPinch.bind(this));
  4986. this.setOptions(options);
  4987. }
  4988. Range.prototype = new Component();
  4989. /**
  4990. * Set options for the range controller
  4991. * @param {Object} options Available options:
  4992. * {Number | Date | String} start Start date for the range
  4993. * {Number | Date | String} end End date for the range
  4994. * {Number} min Minimum value for start
  4995. * {Number} max Maximum value for end
  4996. * {Number} zoomMin Set a minimum value for
  4997. * (end - start).
  4998. * {Number} zoomMax Set a maximum value for
  4999. * (end - start).
  5000. * {Boolean} moveable Enable moving of the range
  5001. * by dragging. True by default
  5002. * {Boolean} zoomable Enable zooming of the range
  5003. * by pinching/scrolling. True by default
  5004. */
  5005. Range.prototype.setOptions = function (options) {
  5006. if (options) {
  5007. // copy the options that we know
  5008. var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
  5009. util.selectiveExtend(fields, this.options, options);
  5010. if ('start' in options || 'end' in options) {
  5011. // apply a new range. both start and end are optional
  5012. this.setRange(options.start, options.end);
  5013. }
  5014. }
  5015. };
  5016. /**
  5017. * Test whether direction has a valid value
  5018. * @param {String} direction 'horizontal' or 'vertical'
  5019. */
  5020. function validateDirection (direction) {
  5021. if (direction != 'horizontal' && direction != 'vertical') {
  5022. throw new TypeError('Unknown direction "' + direction + '". ' +
  5023. 'Choose "horizontal" or "vertical".');
  5024. }
  5025. }
  5026. /**
  5027. * Set a new start and end range
  5028. * @param {Number} [start]
  5029. * @param {Number} [end]
  5030. */
  5031. Range.prototype.setRange = function(start, end) {
  5032. var changed = this._applyRange(start, end);
  5033. if (changed) {
  5034. var params = {
  5035. start: new Date(this.start),
  5036. end: new Date(this.end)
  5037. };
  5038. this.body.emitter.emit('rangechange', params);
  5039. this.body.emitter.emit('rangechanged', params);
  5040. }
  5041. };
  5042. /**
  5043. * Set a new start and end range. This method is the same as setRange, but
  5044. * does not trigger a range change and range changed event, and it returns
  5045. * true when the range is changed
  5046. * @param {Number} [start]
  5047. * @param {Number} [end]
  5048. * @return {Boolean} changed
  5049. * @private
  5050. */
  5051. Range.prototype._applyRange = function(start, end) {
  5052. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  5053. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  5054. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  5055. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  5056. diff;
  5057. // check for valid number
  5058. if (isNaN(newStart) || newStart === null) {
  5059. throw new Error('Invalid start "' + start + '"');
  5060. }
  5061. if (isNaN(newEnd) || newEnd === null) {
  5062. throw new Error('Invalid end "' + end + '"');
  5063. }
  5064. // prevent start < end
  5065. if (newEnd < newStart) {
  5066. newEnd = newStart;
  5067. }
  5068. // prevent start < min
  5069. if (min !== null) {
  5070. if (newStart < min) {
  5071. diff = (min - newStart);
  5072. newStart += diff;
  5073. newEnd += diff;
  5074. // prevent end > max
  5075. if (max != null) {
  5076. if (newEnd > max) {
  5077. newEnd = max;
  5078. }
  5079. }
  5080. }
  5081. }
  5082. // prevent end > max
  5083. if (max !== null) {
  5084. if (newEnd > max) {
  5085. diff = (newEnd - max);
  5086. newStart -= diff;
  5087. newEnd -= diff;
  5088. // prevent start < min
  5089. if (min != null) {
  5090. if (newStart < min) {
  5091. newStart = min;
  5092. }
  5093. }
  5094. }
  5095. }
  5096. // prevent (end-start) < zoomMin
  5097. if (this.options.zoomMin !== null) {
  5098. var zoomMin = parseFloat(this.options.zoomMin);
  5099. if (zoomMin < 0) {
  5100. zoomMin = 0;
  5101. }
  5102. if ((newEnd - newStart) < zoomMin) {
  5103. if ((this.end - this.start) === zoomMin) {
  5104. // ignore this action, we are already zoomed to the minimum
  5105. newStart = this.start;
  5106. newEnd = this.end;
  5107. }
  5108. else {
  5109. // zoom to the minimum
  5110. diff = (zoomMin - (newEnd - newStart));
  5111. newStart -= diff / 2;
  5112. newEnd += diff / 2;
  5113. }
  5114. }
  5115. }
  5116. // prevent (end-start) > zoomMax
  5117. if (this.options.zoomMax !== null) {
  5118. var zoomMax = parseFloat(this.options.zoomMax);
  5119. if (zoomMax < 0) {
  5120. zoomMax = 0;
  5121. }
  5122. if ((newEnd - newStart) > zoomMax) {
  5123. if ((this.end - this.start) === zoomMax) {
  5124. // ignore this action, we are already zoomed to the maximum
  5125. newStart = this.start;
  5126. newEnd = this.end;
  5127. }
  5128. else {
  5129. // zoom to the maximum
  5130. diff = ((newEnd - newStart) - zoomMax);
  5131. newStart += diff / 2;
  5132. newEnd -= diff / 2;
  5133. }
  5134. }
  5135. }
  5136. var changed = (this.start != newStart || this.end != newEnd);
  5137. this.start = newStart;
  5138. this.end = newEnd;
  5139. return changed;
  5140. };
  5141. /**
  5142. * Retrieve the current range.
  5143. * @return {Object} An object with start and end properties
  5144. */
  5145. Range.prototype.getRange = function() {
  5146. return {
  5147. start: this.start,
  5148. end: this.end
  5149. };
  5150. };
  5151. /**
  5152. * Calculate the conversion offset and scale for current range, based on
  5153. * the provided width
  5154. * @param {Number} width
  5155. * @returns {{offset: number, scale: number}} conversion
  5156. */
  5157. Range.prototype.conversion = function (width) {
  5158. return Range.conversion(this.start, this.end, width);
  5159. };
  5160. /**
  5161. * Static method to calculate the conversion offset and scale for a range,
  5162. * based on the provided start, end, and width
  5163. * @param {Number} start
  5164. * @param {Number} end
  5165. * @param {Number} width
  5166. * @returns {{offset: number, scale: number}} conversion
  5167. */
  5168. Range.conversion = function (start, end, width) {
  5169. if (width != 0 && (end - start != 0)) {
  5170. return {
  5171. offset: start,
  5172. scale: width / (end - start)
  5173. }
  5174. }
  5175. else {
  5176. return {
  5177. offset: 0,
  5178. scale: 1
  5179. };
  5180. }
  5181. };
  5182. /**
  5183. * Start dragging horizontally or vertically
  5184. * @param {Event} event
  5185. * @private
  5186. */
  5187. Range.prototype._onDragStart = function(event) {
  5188. // only allow dragging when configured as movable
  5189. if (!this.options.moveable) return;
  5190. // refuse to drag when we where pinching to prevent the timeline make a jump
  5191. // when releasing the fingers in opposite order from the touch screen
  5192. if (!this.props.touch.allowDragging) return;
  5193. this.props.touch.start = this.start;
  5194. this.props.touch.end = this.end;
  5195. if (this.body.dom.root) {
  5196. this.body.dom.root.style.cursor = 'move';
  5197. }
  5198. };
  5199. /**
  5200. * Perform dragging operation
  5201. * @param {Event} event
  5202. * @private
  5203. */
  5204. Range.prototype._onDrag = function (event) {
  5205. // only allow dragging when configured as movable
  5206. if (!this.options.moveable) return;
  5207. var direction = this.options.direction;
  5208. validateDirection(direction);
  5209. // refuse to drag when we where pinching to prevent the timeline make a jump
  5210. // when releasing the fingers in opposite order from the touch screen
  5211. if (!this.props.touch.allowDragging) return;
  5212. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  5213. interval = (this.props.touch.end - this.props.touch.start),
  5214. width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
  5215. diffRange = -delta / width * interval;
  5216. this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
  5217. this.body.emitter.emit('rangechange', {
  5218. start: new Date(this.start),
  5219. end: new Date(this.end)
  5220. });
  5221. };
  5222. /**
  5223. * Stop dragging operation
  5224. * @param {event} event
  5225. * @private
  5226. */
  5227. Range.prototype._onDragEnd = function (event) {
  5228. // only allow dragging when configured as movable
  5229. if (!this.options.moveable) return;
  5230. // refuse to drag when we where pinching to prevent the timeline make a jump
  5231. // when releasing the fingers in opposite order from the touch screen
  5232. if (!this.props.touch.allowDragging) return;
  5233. if (this.body.dom.root) {
  5234. this.body.dom.root.style.cursor = 'auto';
  5235. }
  5236. // fire a rangechanged event
  5237. this.body.emitter.emit('rangechanged', {
  5238. start: new Date(this.start),
  5239. end: new Date(this.end)
  5240. });
  5241. };
  5242. /**
  5243. * Event handler for mouse wheel event, used to zoom
  5244. * Code from http://adomas.org/javascript-mouse-wheel/
  5245. * @param {Event} event
  5246. * @private
  5247. */
  5248. Range.prototype._onMouseWheel = function(event) {
  5249. // only allow zooming when configured as zoomable and moveable
  5250. if (!(this.options.zoomable && this.options.moveable)) return;
  5251. // retrieve delta
  5252. var delta = 0;
  5253. if (event.wheelDelta) { /* IE/Opera. */
  5254. delta = event.wheelDelta / 120;
  5255. } else if (event.detail) { /* Mozilla case. */
  5256. // In Mozilla, sign of delta is different than in IE.
  5257. // Also, delta is multiple of 3.
  5258. delta = -event.detail / 3;
  5259. }
  5260. // If delta is nonzero, handle it.
  5261. // Basically, delta is now positive if wheel was scrolled up,
  5262. // and negative, if wheel was scrolled down.
  5263. if (delta) {
  5264. // perform the zoom action. Delta is normally 1 or -1
  5265. // adjust a negative delta such that zooming in with delta 0.1
  5266. // equals zooming out with a delta -0.1
  5267. var scale;
  5268. if (delta < 0) {
  5269. scale = 1 - (delta / 5);
  5270. }
  5271. else {
  5272. scale = 1 / (1 + (delta / 5)) ;
  5273. }
  5274. // calculate center, the date to zoom around
  5275. var gesture = util.fakeGesture(this, event),
  5276. pointer = getPointer(gesture.center, this.body.dom.center),
  5277. pointerDate = this._pointerToDate(pointer);
  5278. this.zoom(scale, pointerDate);
  5279. }
  5280. // Prevent default actions caused by mouse wheel
  5281. // (else the page and timeline both zoom and scroll)
  5282. event.preventDefault();
  5283. };
  5284. /**
  5285. * Start of a touch gesture
  5286. * @private
  5287. */
  5288. Range.prototype._onTouch = function (event) {
  5289. this.props.touch.start = this.start;
  5290. this.props.touch.end = this.end;
  5291. this.props.touch.allowDragging = true;
  5292. this.props.touch.center = null;
  5293. };
  5294. /**
  5295. * On start of a hold gesture
  5296. * @private
  5297. */
  5298. Range.prototype._onHold = function () {
  5299. this.props.touch.allowDragging = false;
  5300. };
  5301. /**
  5302. * Handle pinch event
  5303. * @param {Event} event
  5304. * @private
  5305. */
  5306. Range.prototype._onPinch = function (event) {
  5307. // only allow zooming when configured as zoomable and moveable
  5308. if (!(this.options.zoomable && this.options.moveable)) return;
  5309. this.props.touch.allowDragging = false;
  5310. if (event.gesture.touches.length > 1) {
  5311. if (!this.props.touch.center) {
  5312. this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
  5313. }
  5314. var scale = 1 / event.gesture.scale,
  5315. initDate = this._pointerToDate(this.props.touch.center);
  5316. // calculate new start and end
  5317. var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
  5318. var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
  5319. // apply new range
  5320. this.setRange(newStart, newEnd);
  5321. }
  5322. };
  5323. /**
  5324. * Helper function to calculate the center date for zooming
  5325. * @param {{x: Number, y: Number}} pointer
  5326. * @return {number} date
  5327. * @private
  5328. */
  5329. Range.prototype._pointerToDate = function (pointer) {
  5330. var conversion;
  5331. var direction = this.options.direction;
  5332. validateDirection(direction);
  5333. if (direction == 'horizontal') {
  5334. var width = this.body.domProps.center.width;
  5335. conversion = this.conversion(width);
  5336. return pointer.x / conversion.scale + conversion.offset;
  5337. }
  5338. else {
  5339. var height = this.body.domProps.center.height;
  5340. conversion = this.conversion(height);
  5341. return pointer.y / conversion.scale + conversion.offset;
  5342. }
  5343. };
  5344. /**
  5345. * Get the pointer location relative to the location of the dom element
  5346. * @param {{pageX: Number, pageY: Number}} touch
  5347. * @param {Element} element HTML DOM element
  5348. * @return {{x: Number, y: Number}} pointer
  5349. * @private
  5350. */
  5351. function getPointer (touch, element) {
  5352. return {
  5353. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  5354. y: touch.pageY - vis.util.getAbsoluteTop(element)
  5355. };
  5356. }
  5357. /**
  5358. * Zoom the range the given scale in or out. Start and end date will
  5359. * be adjusted, and the timeline will be redrawn. You can optionally give a
  5360. * date around which to zoom.
  5361. * For example, try scale = 0.9 or 1.1
  5362. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  5363. * values below 1 will zoom in.
  5364. * @param {Number} [center] Value representing a date around which will
  5365. * be zoomed.
  5366. */
  5367. Range.prototype.zoom = function(scale, center) {
  5368. // if centerDate is not provided, take it half between start Date and end Date
  5369. if (center == null) {
  5370. center = (this.start + this.end) / 2;
  5371. }
  5372. // calculate new start and end
  5373. var newStart = center + (this.start - center) * scale;
  5374. var newEnd = center + (this.end - center) * scale;
  5375. this.setRange(newStart, newEnd);
  5376. };
  5377. /**
  5378. * Move the range with a given delta to the left or right. Start and end
  5379. * value will be adjusted. For example, try delta = 0.1 or -0.1
  5380. * @param {Number} delta Moving amount. Positive value will move right,
  5381. * negative value will move left
  5382. */
  5383. Range.prototype.move = function(delta) {
  5384. // zoom start Date and end Date relative to the centerDate
  5385. var diff = (this.end - this.start);
  5386. // apply new values
  5387. var newStart = this.start + diff * delta;
  5388. var newEnd = this.end + diff * delta;
  5389. // TODO: reckon with min and max range
  5390. this.start = newStart;
  5391. this.end = newEnd;
  5392. };
  5393. /**
  5394. * Move the range to a new center point
  5395. * @param {Number} moveTo New center point of the range
  5396. */
  5397. Range.prototype.moveTo = function(moveTo) {
  5398. var center = (this.start + this.end) / 2;
  5399. var diff = center - moveTo;
  5400. // calculate new start and end
  5401. var newStart = this.start - diff;
  5402. var newEnd = this.end - diff;
  5403. this.setRange(newStart, newEnd);
  5404. };
  5405. /**
  5406. * Prototype for visual components
  5407. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]
  5408. * @param {Object} [options]
  5409. */
  5410. function Component (body, options) {
  5411. this.options = null;
  5412. this.props = null;
  5413. }
  5414. /**
  5415. * Set options for the component. The new options will be merged into the
  5416. * current options.
  5417. * @param {Object} options
  5418. */
  5419. Component.prototype.setOptions = function(options) {
  5420. if (options) {
  5421. util.extend(this.options, options);
  5422. }
  5423. };
  5424. /**
  5425. * Repaint the component
  5426. * @return {boolean} Returns true if the component is resized
  5427. */
  5428. Component.prototype.redraw = function() {
  5429. // should be implemented by the component
  5430. return false;
  5431. };
  5432. /**
  5433. * Destroy the component. Cleanup DOM and event listeners
  5434. */
  5435. Component.prototype.destroy = function() {
  5436. // should be implemented by the component
  5437. };
  5438. /**
  5439. * Test whether the component is resized since the last time _isResized() was
  5440. * called.
  5441. * @return {Boolean} Returns true if the component is resized
  5442. * @protected
  5443. */
  5444. Component.prototype._isResized = function() {
  5445. var resized = (this.props._previousWidth !== this.props.width ||
  5446. this.props._previousHeight !== this.props.height);
  5447. this.props._previousWidth = this.props.width;
  5448. this.props._previousHeight = this.props.height;
  5449. return resized;
  5450. };
  5451. /**
  5452. * A horizontal time axis
  5453. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
  5454. * @param {Object} [options] See TimeAxis.setOptions for the available
  5455. * options.
  5456. * @constructor TimeAxis
  5457. * @extends Component
  5458. */
  5459. function TimeAxis (body, options) {
  5460. this.dom = {
  5461. foreground: null,
  5462. majorLines: [],
  5463. majorTexts: [],
  5464. minorLines: [],
  5465. minorTexts: [],
  5466. redundant: {
  5467. majorLines: [],
  5468. majorTexts: [],
  5469. minorLines: [],
  5470. minorTexts: []
  5471. }
  5472. };
  5473. this.props = {
  5474. range: {
  5475. start: 0,
  5476. end: 0,
  5477. minimumStep: 0
  5478. },
  5479. lineTop: 0
  5480. };
  5481. this.defaultOptions = {
  5482. orientation: 'bottom', // supported: 'top', 'bottom'
  5483. // TODO: implement timeaxis orientations 'left' and 'right'
  5484. showMinorLabels: true,
  5485. showMajorLabels: true
  5486. };
  5487. this.options = util.extend({}, this.defaultOptions);
  5488. this.body = body;
  5489. // create the HTML DOM
  5490. this._create();
  5491. this.setOptions(options);
  5492. }
  5493. TimeAxis.prototype = new Component();
  5494. /**
  5495. * Set options for the TimeAxis.
  5496. * Parameters will be merged in current options.
  5497. * @param {Object} options Available options:
  5498. * {string} [orientation]
  5499. * {boolean} [showMinorLabels]
  5500. * {boolean} [showMajorLabels]
  5501. */
  5502. TimeAxis.prototype.setOptions = function(options) {
  5503. if (options) {
  5504. // copy all options that we know
  5505. util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
  5506. }
  5507. };
  5508. /**
  5509. * Create the HTML DOM for the TimeAxis
  5510. */
  5511. TimeAxis.prototype._create = function() {
  5512. this.dom.foreground = document.createElement('div');
  5513. this.dom.background = document.createElement('div');
  5514. this.dom.foreground.className = 'timeaxis foreground';
  5515. this.dom.background.className = 'timeaxis background';
  5516. };
  5517. /**
  5518. * Destroy the TimeAxis
  5519. */
  5520. TimeAxis.prototype.destroy = function() {
  5521. // remove from DOM
  5522. if (this.dom.foreground.parentNode) {
  5523. this.dom.foreground.parentNode.removeChild(this.dom.foreground);
  5524. }
  5525. if (this.dom.background.parentNode) {
  5526. this.dom.background.parentNode.removeChild(this.dom.background);
  5527. }
  5528. this.body = null;
  5529. };
  5530. /**
  5531. * Repaint the component
  5532. * @return {boolean} Returns true if the component is resized
  5533. */
  5534. TimeAxis.prototype.redraw = function () {
  5535. var options = this.options,
  5536. props = this.props,
  5537. foreground = this.dom.foreground,
  5538. background = this.dom.background;
  5539. // determine the correct parent DOM element (depending on option orientation)
  5540. var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom;
  5541. var parentChanged = (foreground.parentNode !== parent);
  5542. // calculate character width and height
  5543. this._calculateCharSize();
  5544. // TODO: recalculate sizes only needed when parent is resized or options is changed
  5545. var orientation = this.options.orientation,
  5546. showMinorLabels = this.options.showMinorLabels,
  5547. showMajorLabels = this.options.showMajorLabels;
  5548. // determine the width and height of the elemens for the axis
  5549. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  5550. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  5551. props.height = props.minorLabelHeight + props.majorLabelHeight;
  5552. props.width = foreground.offsetWidth;
  5553. props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
  5554. (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
  5555. props.minorLineWidth = 1; // TODO: really calculate width
  5556. props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
  5557. props.majorLineWidth = 1; // TODO: really calculate width
  5558. // take foreground and background offline while updating (is almost twice as fast)
  5559. var foregroundNextSibling = foreground.nextSibling;
  5560. var backgroundNextSibling = background.nextSibling;
  5561. foreground.parentNode && foreground.parentNode.removeChild(foreground);
  5562. background.parentNode && background.parentNode.removeChild(background);
  5563. foreground.style.height = this.props.height + 'px';
  5564. this._repaintLabels();
  5565. // put DOM online again (at the same place)
  5566. if (foregroundNextSibling) {
  5567. parent.insertBefore(foreground, foregroundNextSibling);
  5568. }
  5569. else {
  5570. parent.appendChild(foreground)
  5571. }
  5572. if (backgroundNextSibling) {
  5573. this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
  5574. }
  5575. else {
  5576. this.body.dom.backgroundVertical.appendChild(background)
  5577. }
  5578. return this._isResized() || parentChanged;
  5579. };
  5580. /**
  5581. * Repaint major and minor text labels and vertical grid lines
  5582. * @private
  5583. */
  5584. TimeAxis.prototype._repaintLabels = function () {
  5585. var orientation = this.options.orientation;
  5586. // calculate range and step (step such that we have space for 7 characters per label)
  5587. var start = util.convert(this.body.range.start, 'Number'),
  5588. end = util.convert(this.body.range.end, 'Number'),
  5589. minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
  5590. -this.body.util.toTime(0).valueOf();
  5591. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  5592. this.step = step;
  5593. // Move all DOM elements to a "redundant" list, where they
  5594. // can be picked for re-use, and clear the lists with lines and texts.
  5595. // At the end of the function _repaintLabels, left over elements will be cleaned up
  5596. var dom = this.dom;
  5597. dom.redundant.majorLines = dom.majorLines;
  5598. dom.redundant.majorTexts = dom.majorTexts;
  5599. dom.redundant.minorLines = dom.minorLines;
  5600. dom.redundant.minorTexts = dom.minorTexts;
  5601. dom.majorLines = [];
  5602. dom.majorTexts = [];
  5603. dom.minorLines = [];
  5604. dom.minorTexts = [];
  5605. step.first();
  5606. var xFirstMajorLabel = undefined;
  5607. var max = 0;
  5608. while (step.hasNext() && max < 1000) {
  5609. max++;
  5610. var cur = step.getCurrent(),
  5611. x = this.body.util.toScreen(cur),
  5612. isMajor = step.isMajor();
  5613. // TODO: lines must have a width, such that we can create css backgrounds
  5614. if (this.options.showMinorLabels) {
  5615. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  5616. }
  5617. if (isMajor && this.options.showMajorLabels) {
  5618. if (x > 0) {
  5619. if (xFirstMajorLabel == undefined) {
  5620. xFirstMajorLabel = x;
  5621. }
  5622. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  5623. }
  5624. this._repaintMajorLine(x, orientation);
  5625. }
  5626. else {
  5627. this._repaintMinorLine(x, orientation);
  5628. }
  5629. step.next();
  5630. }
  5631. // create a major label on the left when needed
  5632. if (this.options.showMajorLabels) {
  5633. var leftTime = this.body.util.toTime(0),
  5634. leftText = step.getLabelMajor(leftTime),
  5635. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  5636. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  5637. this._repaintMajorText(0, leftText, orientation);
  5638. }
  5639. }
  5640. // Cleanup leftover DOM elements from the redundant list
  5641. util.forEach(this.dom.redundant, function (arr) {
  5642. while (arr.length) {
  5643. var elem = arr.pop();
  5644. if (elem && elem.parentNode) {
  5645. elem.parentNode.removeChild(elem);
  5646. }
  5647. }
  5648. });
  5649. };
  5650. /**
  5651. * Create a minor label for the axis at position x
  5652. * @param {Number} x
  5653. * @param {String} text
  5654. * @param {String} orientation "top" or "bottom" (default)
  5655. * @private
  5656. */
  5657. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  5658. // reuse redundant label
  5659. var label = this.dom.redundant.minorTexts.shift();
  5660. if (!label) {
  5661. // create new label
  5662. var content = document.createTextNode('');
  5663. label = document.createElement('div');
  5664. label.appendChild(content);
  5665. label.className = 'text minor';
  5666. this.dom.foreground.appendChild(label);
  5667. }
  5668. this.dom.minorTexts.push(label);
  5669. label.childNodes[0].nodeValue = text;
  5670. label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
  5671. label.style.left = x + 'px';
  5672. //label.title = title; // TODO: this is a heavy operation
  5673. };
  5674. /**
  5675. * Create a Major label for the axis at position x
  5676. * @param {Number} x
  5677. * @param {String} text
  5678. * @param {String} orientation "top" or "bottom" (default)
  5679. * @private
  5680. */
  5681. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  5682. // reuse redundant label
  5683. var label = this.dom.redundant.majorTexts.shift();
  5684. if (!label) {
  5685. // create label
  5686. var content = document.createTextNode(text);
  5687. label = document.createElement('div');
  5688. label.className = 'text major';
  5689. label.appendChild(content);
  5690. this.dom.foreground.appendChild(label);
  5691. }
  5692. this.dom.majorTexts.push(label);
  5693. label.childNodes[0].nodeValue = text;
  5694. //label.title = title; // TODO: this is a heavy operation
  5695. label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
  5696. label.style.left = x + 'px';
  5697. };
  5698. /**
  5699. * Create a minor line for the axis at position x
  5700. * @param {Number} x
  5701. * @param {String} orientation "top" or "bottom" (default)
  5702. * @private
  5703. */
  5704. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  5705. // reuse redundant line
  5706. var line = this.dom.redundant.minorLines.shift();
  5707. if (!line) {
  5708. // create vertical line
  5709. line = document.createElement('div');
  5710. line.className = 'grid vertical minor';
  5711. this.dom.background.appendChild(line);
  5712. }
  5713. this.dom.minorLines.push(line);
  5714. var props = this.props;
  5715. if (orientation == 'top') {
  5716. line.style.top = props.majorLabelHeight + 'px';
  5717. }
  5718. else {
  5719. line.style.top = this.body.domProps.top.height + 'px';
  5720. }
  5721. line.style.height = props.minorLineHeight + 'px';
  5722. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  5723. };
  5724. /**
  5725. * Create a Major line for the axis at position x
  5726. * @param {Number} x
  5727. * @param {String} orientation "top" or "bottom" (default)
  5728. * @private
  5729. */
  5730. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  5731. // reuse redundant line
  5732. var line = this.dom.redundant.majorLines.shift();
  5733. if (!line) {
  5734. // create vertical line
  5735. line = document.createElement('DIV');
  5736. line.className = 'grid vertical major';
  5737. this.dom.background.appendChild(line);
  5738. }
  5739. this.dom.majorLines.push(line);
  5740. var props = this.props;
  5741. if (orientation == 'top') {
  5742. line.style.top = '0';
  5743. }
  5744. else {
  5745. line.style.top = this.body.domProps.top.height + 'px';
  5746. }
  5747. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  5748. line.style.height = props.majorLineHeight + 'px';
  5749. };
  5750. /**
  5751. * Determine the size of text on the axis (both major and minor axis).
  5752. * The size is calculated only once and then cached in this.props.
  5753. * @private
  5754. */
  5755. TimeAxis.prototype._calculateCharSize = function () {
  5756. // Note: We calculate char size with every redraw. Size may change, for
  5757. // example when any of the timelines parents had display:none for example.
  5758. // determine the char width and height on the minor axis
  5759. if (!this.dom.measureCharMinor) {
  5760. this.dom.measureCharMinor = document.createElement('DIV');
  5761. this.dom.measureCharMinor.className = 'text minor measure';
  5762. this.dom.measureCharMinor.style.position = 'absolute';
  5763. this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
  5764. this.dom.foreground.appendChild(this.dom.measureCharMinor);
  5765. }
  5766. this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
  5767. this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
  5768. // determine the char width and height on the major axis
  5769. if (!this.dom.measureCharMajor) {
  5770. this.dom.measureCharMajor = document.createElement('DIV');
  5771. this.dom.measureCharMajor.className = 'text minor measure';
  5772. this.dom.measureCharMajor.style.position = 'absolute';
  5773. this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
  5774. this.dom.foreground.appendChild(this.dom.measureCharMajor);
  5775. }
  5776. this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
  5777. this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
  5778. };
  5779. /**
  5780. * Snap a date to a rounded value.
  5781. * The snap intervals are dependent on the current scale and step.
  5782. * @param {Date} date the date to be snapped.
  5783. * @return {Date} snappedDate
  5784. */
  5785. TimeAxis.prototype.snap = function(date) {
  5786. return this.step.snap(date);
  5787. };
  5788. /**
  5789. * A current time bar
  5790. * @param {{range: Range, dom: Object, domProps: Object}} body
  5791. * @param {Object} [options] Available parameters:
  5792. * {Boolean} [showCurrentTime]
  5793. * @constructor CurrentTime
  5794. * @extends Component
  5795. */
  5796. function CurrentTime (body, options) {
  5797. this.body = body;
  5798. // default options
  5799. this.defaultOptions = {
  5800. showCurrentTime: true
  5801. };
  5802. this.options = util.extend({}, this.defaultOptions);
  5803. this._create();
  5804. this.setOptions(options);
  5805. }
  5806. CurrentTime.prototype = new Component();
  5807. /**
  5808. * Create the HTML DOM for the current time bar
  5809. * @private
  5810. */
  5811. CurrentTime.prototype._create = function() {
  5812. var bar = document.createElement('div');
  5813. bar.className = 'currenttime';
  5814. bar.style.position = 'absolute';
  5815. bar.style.top = '0px';
  5816. bar.style.height = '100%';
  5817. this.bar = bar;
  5818. };
  5819. /**
  5820. * Destroy the CurrentTime bar
  5821. */
  5822. CurrentTime.prototype.destroy = function () {
  5823. this.options.showCurrentTime = false;
  5824. this.redraw(); // will remove the bar from the DOM and stop refreshing
  5825. this.body = null;
  5826. };
  5827. /**
  5828. * Set options for the component. Options will be merged in current options.
  5829. * @param {Object} options Available parameters:
  5830. * {boolean} [showCurrentTime]
  5831. */
  5832. CurrentTime.prototype.setOptions = function(options) {
  5833. if (options) {
  5834. // copy all options that we know
  5835. util.selectiveExtend(['showCurrentTime'], this.options, options);
  5836. }
  5837. };
  5838. /**
  5839. * Repaint the component
  5840. * @return {boolean} Returns true if the component is resized
  5841. */
  5842. CurrentTime.prototype.redraw = function() {
  5843. if (this.options.showCurrentTime) {
  5844. var parent = this.body.dom.backgroundVertical;
  5845. if (this.bar.parentNode != parent) {
  5846. // attach to the dom
  5847. if (this.bar.parentNode) {
  5848. this.bar.parentNode.removeChild(this.bar);
  5849. }
  5850. parent.appendChild(this.bar);
  5851. this.start();
  5852. }
  5853. var now = new Date();
  5854. var x = this.body.util.toScreen(now);
  5855. this.bar.style.left = x + 'px';
  5856. this.bar.title = 'Current time: ' + now;
  5857. }
  5858. else {
  5859. // remove the line from the DOM
  5860. if (this.bar.parentNode) {
  5861. this.bar.parentNode.removeChild(this.bar);
  5862. }
  5863. this.stop();
  5864. }
  5865. return false;
  5866. };
  5867. /**
  5868. * Start auto refreshing the current time bar
  5869. */
  5870. CurrentTime.prototype.start = function() {
  5871. var me = this;
  5872. function update () {
  5873. me.stop();
  5874. // determine interval to refresh
  5875. var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
  5876. var interval = 1 / scale / 10;
  5877. if (interval < 30) interval = 30;
  5878. if (interval > 1000) interval = 1000;
  5879. me.redraw();
  5880. // start a timer to adjust for the new time
  5881. me.currentTimeTimer = setTimeout(update, interval);
  5882. }
  5883. update();
  5884. };
  5885. /**
  5886. * Stop auto refreshing the current time bar
  5887. */
  5888. CurrentTime.prototype.stop = function() {
  5889. if (this.currentTimeTimer !== undefined) {
  5890. clearTimeout(this.currentTimeTimer);
  5891. delete this.currentTimeTimer;
  5892. }
  5893. };
  5894. /**
  5895. * A custom time bar
  5896. * @param {{range: Range, dom: Object}} body
  5897. * @param {Object} [options] Available parameters:
  5898. * {Boolean} [showCustomTime]
  5899. * @constructor CustomTime
  5900. * @extends Component
  5901. */
  5902. function CustomTime (body, options) {
  5903. this.body = body;
  5904. // default options
  5905. this.defaultOptions = {
  5906. showCustomTime: false
  5907. };
  5908. this.options = util.extend({}, this.defaultOptions);
  5909. this.customTime = new Date();
  5910. this.eventParams = {}; // stores state parameters while dragging the bar
  5911. // create the DOM
  5912. this._create();
  5913. this.setOptions(options);
  5914. }
  5915. CustomTime.prototype = new Component();
  5916. /**
  5917. * Set options for the component. Options will be merged in current options.
  5918. * @param {Object} options Available parameters:
  5919. * {boolean} [showCustomTime]
  5920. */
  5921. CustomTime.prototype.setOptions = function(options) {
  5922. if (options) {
  5923. // copy all options that we know
  5924. util.selectiveExtend(['showCustomTime'], this.options, options);
  5925. }
  5926. };
  5927. /**
  5928. * Create the DOM for the custom time
  5929. * @private
  5930. */
  5931. CustomTime.prototype._create = function() {
  5932. var bar = document.createElement('div');
  5933. bar.className = 'customtime';
  5934. bar.style.position = 'absolute';
  5935. bar.style.top = '0px';
  5936. bar.style.height = '100%';
  5937. this.bar = bar;
  5938. var drag = document.createElement('div');
  5939. drag.style.position = 'relative';
  5940. drag.style.top = '0px';
  5941. drag.style.left = '-10px';
  5942. drag.style.height = '100%';
  5943. drag.style.width = '20px';
  5944. bar.appendChild(drag);
  5945. // attach event listeners
  5946. this.hammer = Hammer(bar, {
  5947. prevent_default: true
  5948. });
  5949. this.hammer.on('dragstart', this._onDragStart.bind(this));
  5950. this.hammer.on('drag', this._onDrag.bind(this));
  5951. this.hammer.on('dragend', this._onDragEnd.bind(this));
  5952. };
  5953. /**
  5954. * Destroy the CustomTime bar
  5955. */
  5956. CustomTime.prototype.destroy = function () {
  5957. this.options.showCustomTime = false;
  5958. this.redraw(); // will remove the bar from the DOM
  5959. this.hammer.enable(false);
  5960. this.hammer = null;
  5961. this.body = null;
  5962. };
  5963. /**
  5964. * Repaint the component
  5965. * @return {boolean} Returns true if the component is resized
  5966. */
  5967. CustomTime.prototype.redraw = function () {
  5968. if (this.options.showCustomTime) {
  5969. var parent = this.body.dom.backgroundVertical;
  5970. if (this.bar.parentNode != parent) {
  5971. // attach to the dom
  5972. if (this.bar.parentNode) {
  5973. this.bar.parentNode.removeChild(this.bar);
  5974. }
  5975. parent.appendChild(this.bar);
  5976. }
  5977. var x = this.body.util.toScreen(this.customTime);
  5978. this.bar.style.left = x + 'px';
  5979. this.bar.title = 'Time: ' + this.customTime;
  5980. }
  5981. else {
  5982. // remove the line from the DOM
  5983. if (this.bar.parentNode) {
  5984. this.bar.parentNode.removeChild(this.bar);
  5985. }
  5986. }
  5987. return false;
  5988. };
  5989. /**
  5990. * Set custom time.
  5991. * @param {Date} time
  5992. */
  5993. CustomTime.prototype.setCustomTime = function(time) {
  5994. this.customTime = new Date(time.valueOf());
  5995. this.redraw();
  5996. };
  5997. /**
  5998. * Retrieve the current custom time.
  5999. * @return {Date} customTime
  6000. */
  6001. CustomTime.prototype.getCustomTime = function() {
  6002. return new Date(this.customTime.valueOf());
  6003. };
  6004. /**
  6005. * Start moving horizontally
  6006. * @param {Event} event
  6007. * @private
  6008. */
  6009. CustomTime.prototype._onDragStart = function(event) {
  6010. this.eventParams.dragging = true;
  6011. this.eventParams.customTime = this.customTime;
  6012. event.stopPropagation();
  6013. event.preventDefault();
  6014. };
  6015. /**
  6016. * Perform moving operating.
  6017. * @param {Event} event
  6018. * @private
  6019. */
  6020. CustomTime.prototype._onDrag = function (event) {
  6021. if (!this.eventParams.dragging) return;
  6022. var deltaX = event.gesture.deltaX,
  6023. x = this.body.util.toScreen(this.eventParams.customTime) + deltaX,
  6024. time = this.body.util.toTime(x);
  6025. this.setCustomTime(time);
  6026. // fire a timechange event
  6027. this.body.emitter.emit('timechange', {
  6028. time: new Date(this.customTime.valueOf())
  6029. });
  6030. event.stopPropagation();
  6031. event.preventDefault();
  6032. };
  6033. /**
  6034. * Stop moving operating.
  6035. * @param {event} event
  6036. * @private
  6037. */
  6038. CustomTime.prototype._onDragEnd = function (event) {
  6039. if (!this.eventParams.dragging) return;
  6040. // fire a timechanged event
  6041. this.body.emitter.emit('timechanged', {
  6042. time: new Date(this.customTime.valueOf())
  6043. });
  6044. event.stopPropagation();
  6045. event.preventDefault();
  6046. };
  6047. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  6048. /**
  6049. * An ItemSet holds a set of items and ranges which can be displayed in a
  6050. * range. The width is determined by the parent of the ItemSet, and the height
  6051. * is determined by the size of the items.
  6052. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
  6053. * @param {Object} [options] See ItemSet.setOptions for the available options.
  6054. * @constructor ItemSet
  6055. * @extends Component
  6056. */
  6057. function ItemSet(body, options) {
  6058. this.body = body;
  6059. this.defaultOptions = {
  6060. type: null, // 'box', 'point', 'range'
  6061. orientation: 'bottom', // 'top' or 'bottom'
  6062. align: 'center', // alignment of box items
  6063. stack: true,
  6064. groupOrder: null,
  6065. selectable: true,
  6066. editable: {
  6067. updateTime: false,
  6068. updateGroup: false,
  6069. add: false,
  6070. remove: false
  6071. },
  6072. onAdd: function (item, callback) {
  6073. callback(item);
  6074. },
  6075. onUpdate: function (item, callback) {
  6076. callback(item);
  6077. },
  6078. onMove: function (item, callback) {
  6079. callback(item);
  6080. },
  6081. onRemove: function (item, callback) {
  6082. callback(item);
  6083. },
  6084. margin: {
  6085. item: 10,
  6086. axis: 20
  6087. },
  6088. padding: 5
  6089. };
  6090. // options is shared by this ItemSet and all its items
  6091. this.options = util.extend({}, this.defaultOptions);
  6092. // options for getting items from the DataSet with the correct type
  6093. this.itemOptions = {
  6094. type: {start: 'Date', end: 'Date'}
  6095. };
  6096. this.conversion = {
  6097. toScreen: body.util.toScreen,
  6098. toTime: body.util.toTime
  6099. };
  6100. this.dom = {};
  6101. this.props = {};
  6102. this.hammer = null;
  6103. var me = this;
  6104. this.itemsData = null; // DataSet
  6105. this.groupsData = null; // DataSet
  6106. // listeners for the DataSet of the items
  6107. this.itemListeners = {
  6108. 'add': function (event, params, senderId) {
  6109. me._onAdd(params.items);
  6110. },
  6111. 'update': function (event, params, senderId) {
  6112. me._onUpdate(params.items);
  6113. },
  6114. 'remove': function (event, params, senderId) {
  6115. me._onRemove(params.items);
  6116. }
  6117. };
  6118. // listeners for the DataSet of the groups
  6119. this.groupListeners = {
  6120. 'add': function (event, params, senderId) {
  6121. me._onAddGroups(params.items);
  6122. },
  6123. 'update': function (event, params, senderId) {
  6124. me._onUpdateGroups(params.items);
  6125. },
  6126. 'remove': function (event, params, senderId) {
  6127. me._onRemoveGroups(params.items);
  6128. }
  6129. };
  6130. this.items = {}; // object with an Item for every data item
  6131. this.groups = {}; // Group object for every group
  6132. this.groupIds = [];
  6133. this.selection = []; // list with the ids of all selected nodes
  6134. this.stackDirty = true; // if true, all items will be restacked on next redraw
  6135. this.touchParams = {}; // stores properties while dragging
  6136. // create the HTML DOM
  6137. this._create();
  6138. this.setOptions(options);
  6139. }
  6140. ItemSet.prototype = new Component();
  6141. // available item types will be registered here
  6142. ItemSet.types = {
  6143. box: ItemBox,
  6144. range: ItemRange,
  6145. point: ItemPoint
  6146. };
  6147. /**
  6148. * Create the HTML DOM for the ItemSet
  6149. */
  6150. ItemSet.prototype._create = function(){
  6151. var frame = document.createElement('div');
  6152. frame.className = 'itemset';
  6153. frame['timeline-itemset'] = this;
  6154. this.dom.frame = frame;
  6155. // create background panel
  6156. var background = document.createElement('div');
  6157. background.className = 'background';
  6158. frame.appendChild(background);
  6159. this.dom.background = background;
  6160. // create foreground panel
  6161. var foreground = document.createElement('div');
  6162. foreground.className = 'foreground';
  6163. frame.appendChild(foreground);
  6164. this.dom.foreground = foreground;
  6165. // create axis panel
  6166. var axis = document.createElement('div');
  6167. axis.className = 'axis';
  6168. this.dom.axis = axis;
  6169. // create labelset
  6170. var labelSet = document.createElement('div');
  6171. labelSet.className = 'labelset';
  6172. this.dom.labelSet = labelSet;
  6173. // create ungrouped Group
  6174. this._updateUngrouped();
  6175. // attach event listeners
  6176. // Note: we bind to the centerContainer for the case where the height
  6177. // of the center container is larger than of the ItemSet, so we
  6178. // can click in the empty area to create a new item or deselect an item.
  6179. this.hammer = Hammer(this.body.dom.centerContainer, {
  6180. prevent_default: true
  6181. });
  6182. // drag items when selected
  6183. this.hammer.on('touch', this._onTouch.bind(this));
  6184. this.hammer.on('dragstart', this._onDragStart.bind(this));
  6185. this.hammer.on('drag', this._onDrag.bind(this));
  6186. this.hammer.on('dragend', this._onDragEnd.bind(this));
  6187. // single select (or unselect) when tapping an item
  6188. this.hammer.on('tap', this._onSelectItem.bind(this));
  6189. // multi select when holding mouse/touch, or on ctrl+click
  6190. this.hammer.on('hold', this._onMultiSelectItem.bind(this));
  6191. // add item on doubletap
  6192. this.hammer.on('doubletap', this._onAddItem.bind(this));
  6193. // attach to the DOM
  6194. this.show();
  6195. };
  6196. /**
  6197. * Set options for the ItemSet. Existing options will be extended/overwritten.
  6198. * @param {Object} [options] The following options are available:
  6199. * {String} type
  6200. * Default type for the items. Choose from 'box'
  6201. * (default), 'point', or 'range'. The default
  6202. * Style can be overwritten by individual items.
  6203. * {String} align
  6204. * Alignment for the items, only applicable for
  6205. * ItemBox. Choose 'center' (default), 'left', or
  6206. * 'right'.
  6207. * {String} orientation
  6208. * Orientation of the item set. Choose 'top' or
  6209. * 'bottom' (default).
  6210. * {Function} groupOrder
  6211. * A sorting function for ordering groups
  6212. * {Boolean} stack
  6213. * If true (deafult), items will be stacked on
  6214. * top of each other.
  6215. * {Number} margin.axis
  6216. * Margin between the axis and the items in pixels.
  6217. * Default is 20.
  6218. * {Number} margin.item
  6219. * Margin between items in pixels. Default is 10.
  6220. * {Number} margin
  6221. * Set margin for both axis and items in pixels.
  6222. * {Number} padding
  6223. * Padding of the contents of an item in pixels.
  6224. * Must correspond with the items css. Default is 5.
  6225. * {Boolean} selectable
  6226. * If true (default), items can be selected.
  6227. * {Boolean} editable
  6228. * Set all editable options to true or false
  6229. * {Boolean} editable.updateTime
  6230. * Allow dragging an item to an other moment in time
  6231. * {Boolean} editable.updateGroup
  6232. * Allow dragging an item to an other group
  6233. * {Boolean} editable.add
  6234. * Allow creating new items on double tap
  6235. * {Boolean} editable.remove
  6236. * Allow removing items by clicking the delete button
  6237. * top right of a selected item.
  6238. * {Function(item: Item, callback: Function)} onAdd
  6239. * Callback function triggered when an item is about to be added:
  6240. * when the user double taps an empty space in the Timeline.
  6241. * {Function(item: Item, callback: Function)} onUpdate
  6242. * Callback function fired when an item is about to be updated.
  6243. * This function typically has to show a dialog where the user
  6244. * change the item. If not implemented, nothing happens.
  6245. * {Function(item: Item, callback: Function)} onMove
  6246. * Fired when an item has been moved. If not implemented,
  6247. * the move action will be accepted.
  6248. * {Function(item: Item, callback: Function)} onRemove
  6249. * Fired when an item is about to be deleted.
  6250. * If not implemented, the item will be always removed.
  6251. */
  6252. ItemSet.prototype.setOptions = function(options) {
  6253. if (options) {
  6254. // copy all options that we know
  6255. var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder'];
  6256. util.selectiveExtend(fields, this.options, options);
  6257. if ('margin' in options) {
  6258. if (typeof options.margin === 'number') {
  6259. this.options.margin.axis = options.margin;
  6260. this.options.margin.item = options.margin;
  6261. }
  6262. else if (typeof options.margin === 'object'){
  6263. util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin);
  6264. }
  6265. }
  6266. if ('editable' in options) {
  6267. if (typeof options.editable === 'boolean') {
  6268. this.options.editable.updateTime = options.editable;
  6269. this.options.editable.updateGroup = options.editable;
  6270. this.options.editable.add = options.editable;
  6271. this.options.editable.remove = options.editable;
  6272. }
  6273. else if (typeof options.editable === 'object') {
  6274. util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
  6275. }
  6276. }
  6277. // callback functions
  6278. var addCallback = (function (name) {
  6279. if (name in options) {
  6280. var fn = options[name];
  6281. if (!(fn instanceof Function) || fn.length != 2) {
  6282. throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
  6283. }
  6284. this.options[name] = fn;
  6285. }
  6286. }).bind(this);
  6287. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback);
  6288. // force the itemSet to refresh: options like orientation and margins may be changed
  6289. this.markDirty();
  6290. }
  6291. };
  6292. /**
  6293. * Mark the ItemSet dirty so it will refresh everything with next redraw
  6294. */
  6295. ItemSet.prototype.markDirty = function() {
  6296. this.groupIds = [];
  6297. this.stackDirty = true;
  6298. };
  6299. /**
  6300. * Destroy the ItemSet
  6301. */
  6302. ItemSet.prototype.destroy = function() {
  6303. this.hide();
  6304. this.setItems(null);
  6305. this.setGroups(null);
  6306. this.hammer = null;
  6307. this.body = null;
  6308. this.conversion = null;
  6309. };
  6310. /**
  6311. * Hide the component from the DOM
  6312. */
  6313. ItemSet.prototype.hide = function() {
  6314. // remove the frame containing the items
  6315. if (this.dom.frame.parentNode) {
  6316. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6317. }
  6318. // remove the axis with dots
  6319. if (this.dom.axis.parentNode) {
  6320. this.dom.axis.parentNode.removeChild(this.dom.axis);
  6321. }
  6322. // remove the labelset containing all group labels
  6323. if (this.dom.labelSet.parentNode) {
  6324. this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
  6325. }
  6326. };
  6327. /**
  6328. * Show the component in the DOM (when not already visible).
  6329. * @return {Boolean} changed
  6330. */
  6331. ItemSet.prototype.show = function() {
  6332. // show frame containing the items
  6333. if (!this.dom.frame.parentNode) {
  6334. this.body.dom.center.appendChild(this.dom.frame);
  6335. }
  6336. // show axis with dots
  6337. if (!this.dom.axis.parentNode) {
  6338. this.body.dom.backgroundVertical.appendChild(this.dom.axis);
  6339. }
  6340. // show labelset containing labels
  6341. if (!this.dom.labelSet.parentNode) {
  6342. this.body.dom.left.appendChild(this.dom.labelSet);
  6343. }
  6344. };
  6345. /**
  6346. * Set selected items by their id. Replaces the current selection
  6347. * Unknown id's are silently ignored.
  6348. * @param {Array} [ids] An array with zero or more id's of the items to be
  6349. * selected. If ids is an empty array, all items will be
  6350. * unselected.
  6351. */
  6352. ItemSet.prototype.setSelection = function(ids) {
  6353. var i, ii, id, item;
  6354. if (ids) {
  6355. if (!Array.isArray(ids)) {
  6356. throw new TypeError('Array expected');
  6357. }
  6358. // unselect currently selected items
  6359. for (i = 0, ii = this.selection.length; i < ii; i++) {
  6360. id = this.selection[i];
  6361. item = this.items[id];
  6362. if (item) item.unselect();
  6363. }
  6364. // select items
  6365. this.selection = [];
  6366. for (i = 0, ii = ids.length; i < ii; i++) {
  6367. id = ids[i];
  6368. item = this.items[id];
  6369. if (item) {
  6370. this.selection.push(id);
  6371. item.select();
  6372. }
  6373. }
  6374. }
  6375. };
  6376. /**
  6377. * Get the selected items by their id
  6378. * @return {Array} ids The ids of the selected items
  6379. */
  6380. ItemSet.prototype.getSelection = function() {
  6381. return this.selection.concat([]);
  6382. };
  6383. /**
  6384. * Deselect a selected item
  6385. * @param {String | Number} id
  6386. * @private
  6387. */
  6388. ItemSet.prototype._deselect = function(id) {
  6389. var selection = this.selection;
  6390. for (var i = 0, ii = selection.length; i < ii; i++) {
  6391. if (selection[i] == id) { // non-strict comparison!
  6392. selection.splice(i, 1);
  6393. break;
  6394. }
  6395. }
  6396. };
  6397. /**
  6398. * Repaint the component
  6399. * @return {boolean} Returns true if the component is resized
  6400. */
  6401. ItemSet.prototype.redraw = function() {
  6402. var margin = this.options.margin,
  6403. range = this.body.range,
  6404. asSize = util.option.asSize,
  6405. options = this.options,
  6406. orientation = options.orientation,
  6407. resized = false,
  6408. frame = this.dom.frame,
  6409. editable = options.editable.updateTime || options.editable.updateGroup;
  6410. // update class name
  6411. frame.className = 'itemset' + (editable ? ' editable' : '');
  6412. // reorder the groups (if needed)
  6413. resized = this._orderGroups() || resized;
  6414. // check whether zoomed (in that case we need to re-stack everything)
  6415. // TODO: would be nicer to get this as a trigger from Range
  6416. var visibleInterval = range.end - range.start;
  6417. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
  6418. if (zoomed) this.stackDirty = true;
  6419. this.lastVisibleInterval = visibleInterval;
  6420. this.props.lastWidth = this.props.width;
  6421. // redraw all groups
  6422. var restack = this.stackDirty,
  6423. firstGroup = this._firstGroup(),
  6424. firstMargin = {
  6425. item: margin.item,
  6426. axis: margin.axis
  6427. },
  6428. nonFirstMargin = {
  6429. item: margin.item,
  6430. axis: margin.item / 2
  6431. },
  6432. height = 0,
  6433. minHeight = margin.axis + margin.item;
  6434. util.forEach(this.groups, function (group) {
  6435. var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
  6436. var groupResized = group.redraw(range, groupMargin, restack);
  6437. resized = groupResized || resized;
  6438. height += group.height;
  6439. });
  6440. height = Math.max(height, minHeight);
  6441. this.stackDirty = false;
  6442. // update frame height
  6443. frame.style.height = asSize(height);
  6444. // calculate actual size and position
  6445. this.props.top = frame.offsetTop;
  6446. this.props.left = frame.offsetLeft;
  6447. this.props.width = frame.offsetWidth;
  6448. this.props.height = height;
  6449. // reposition axis
  6450. this.dom.axis.style.top = asSize((orientation == 'top') ?
  6451. (this.body.domProps.top.height + this.body.domProps.border.top) :
  6452. (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
  6453. this.dom.axis.style.left = this.body.domProps.border.left + 'px';
  6454. // check if this component is resized
  6455. resized = this._isResized() || resized;
  6456. return resized;
  6457. };
  6458. /**
  6459. * Get the first group, aligned with the axis
  6460. * @return {Group | null} firstGroup
  6461. * @private
  6462. */
  6463. ItemSet.prototype._firstGroup = function() {
  6464. var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
  6465. var firstGroupId = this.groupIds[firstGroupIndex];
  6466. var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
  6467. return firstGroup || null;
  6468. };
  6469. /**
  6470. * Create or delete the group holding all ungrouped items. This group is used when
  6471. * there are no groups specified.
  6472. * @protected
  6473. */
  6474. ItemSet.prototype._updateUngrouped = function() {
  6475. var ungrouped = this.groups[UNGROUPED];
  6476. if (this.groupsData) {
  6477. // remove the group holding all ungrouped items
  6478. if (ungrouped) {
  6479. ungrouped.hide();
  6480. delete this.groups[UNGROUPED];
  6481. }
  6482. }
  6483. else {
  6484. // create a group holding all (unfiltered) items
  6485. if (!ungrouped) {
  6486. var id = null;
  6487. var data = null;
  6488. ungrouped = new Group(id, data, this);
  6489. this.groups[UNGROUPED] = ungrouped;
  6490. for (var itemId in this.items) {
  6491. if (this.items.hasOwnProperty(itemId)) {
  6492. ungrouped.add(this.items[itemId]);
  6493. }
  6494. }
  6495. ungrouped.show();
  6496. }
  6497. }
  6498. };
  6499. /**
  6500. * Get the element for the labelset
  6501. * @return {HTMLElement} labelSet
  6502. */
  6503. ItemSet.prototype.getLabelSet = function() {
  6504. return this.dom.labelSet;
  6505. };
  6506. /**
  6507. * Set items
  6508. * @param {vis.DataSet | null} items
  6509. */
  6510. ItemSet.prototype.setItems = function(items) {
  6511. var me = this,
  6512. ids,
  6513. oldItemsData = this.itemsData;
  6514. // replace the dataset
  6515. if (!items) {
  6516. this.itemsData = null;
  6517. }
  6518. else if (items instanceof DataSet || items instanceof DataView) {
  6519. this.itemsData = items;
  6520. }
  6521. else {
  6522. throw new TypeError('Data must be an instance of DataSet or DataView');
  6523. }
  6524. if (oldItemsData) {
  6525. // unsubscribe from old dataset
  6526. util.forEach(this.itemListeners, function (callback, event) {
  6527. oldItemsData.off(event, callback);
  6528. });
  6529. // remove all drawn items
  6530. ids = oldItemsData.getIds();
  6531. this._onRemove(ids);
  6532. }
  6533. if (this.itemsData) {
  6534. // subscribe to new dataset
  6535. var id = this.id;
  6536. util.forEach(this.itemListeners, function (callback, event) {
  6537. me.itemsData.on(event, callback, id);
  6538. });
  6539. // add all new items
  6540. ids = this.itemsData.getIds();
  6541. this._onAdd(ids);
  6542. // update the group holding all ungrouped items
  6543. this._updateUngrouped();
  6544. }
  6545. };
  6546. /**
  6547. * Get the current items
  6548. * @returns {vis.DataSet | null}
  6549. */
  6550. ItemSet.prototype.getItems = function() {
  6551. return this.itemsData;
  6552. };
  6553. /**
  6554. * Set groups
  6555. * @param {vis.DataSet} groups
  6556. */
  6557. ItemSet.prototype.setGroups = function(groups) {
  6558. var me = this,
  6559. ids;
  6560. // unsubscribe from current dataset
  6561. if (this.groupsData) {
  6562. util.forEach(this.groupListeners, function (callback, event) {
  6563. me.groupsData.unsubscribe(event, callback);
  6564. });
  6565. // remove all drawn groups
  6566. ids = this.groupsData.getIds();
  6567. this.groupsData = null;
  6568. this._onRemoveGroups(ids); // note: this will cause a redraw
  6569. }
  6570. // replace the dataset
  6571. if (!groups) {
  6572. this.groupsData = null;
  6573. }
  6574. else if (groups instanceof DataSet || groups instanceof DataView) {
  6575. this.groupsData = groups;
  6576. }
  6577. else {
  6578. throw new TypeError('Data must be an instance of DataSet or DataView');
  6579. }
  6580. if (this.groupsData) {
  6581. // subscribe to new dataset
  6582. var id = this.id;
  6583. util.forEach(this.groupListeners, function (callback, event) {
  6584. me.groupsData.on(event, callback, id);
  6585. });
  6586. // draw all ms
  6587. ids = this.groupsData.getIds();
  6588. this._onAddGroups(ids);
  6589. }
  6590. // update the group holding all ungrouped items
  6591. this._updateUngrouped();
  6592. // update the order of all items in each group
  6593. this._order();
  6594. this.body.emitter.emit('change');
  6595. };
  6596. /**
  6597. * Get the current groups
  6598. * @returns {vis.DataSet | null} groups
  6599. */
  6600. ItemSet.prototype.getGroups = function() {
  6601. return this.groupsData;
  6602. };
  6603. /**
  6604. * Remove an item by its id
  6605. * @param {String | Number} id
  6606. */
  6607. ItemSet.prototype.removeItem = function(id) {
  6608. var item = this.itemsData.get(id),
  6609. dataset = this._myDataSet();
  6610. if (item) {
  6611. // confirm deletion
  6612. this.options.onRemove(item, function (item) {
  6613. if (item) {
  6614. // remove by id here, it is possible that an item has no id defined
  6615. // itself, so better not delete by the item itself
  6616. dataset.remove(id);
  6617. }
  6618. });
  6619. }
  6620. };
  6621. /**
  6622. * Handle updated items
  6623. * @param {Number[]} ids
  6624. * @protected
  6625. */
  6626. ItemSet.prototype._onUpdate = function(ids) {
  6627. var me = this;
  6628. ids.forEach(function (id) {
  6629. var itemData = me.itemsData.get(id, me.itemOptions),
  6630. item = me.items[id],
  6631. type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box');
  6632. var constructor = ItemSet.types[type];
  6633. if (item) {
  6634. // update item
  6635. if (!constructor || !(item instanceof constructor)) {
  6636. // item type has changed, delete the item and recreate it
  6637. me._removeItem(item);
  6638. item = null;
  6639. }
  6640. else {
  6641. me._updateItem(item, itemData);
  6642. }
  6643. }
  6644. if (!item) {
  6645. // create item
  6646. if (constructor) {
  6647. item = new constructor(itemData, me.conversion, me.options);
  6648. item.id = id; // TODO: not so nice setting id afterwards
  6649. me._addItem(item);
  6650. }
  6651. else if (type == 'rangeoverflow') {
  6652. // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
  6653. throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
  6654. '.vis.timeline .item.range .content {overflow: visible;}');
  6655. }
  6656. else {
  6657. throw new TypeError('Unknown item type "' + type + '"');
  6658. }
  6659. }
  6660. });
  6661. this._order();
  6662. this.stackDirty = true; // force re-stacking of all items next redraw
  6663. this.body.emitter.emit('change');
  6664. };
  6665. /**
  6666. * Handle added items
  6667. * @param {Number[]} ids
  6668. * @protected
  6669. */
  6670. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  6671. /**
  6672. * Handle removed items
  6673. * @param {Number[]} ids
  6674. * @protected
  6675. */
  6676. ItemSet.prototype._onRemove = function(ids) {
  6677. var count = 0;
  6678. var me = this;
  6679. ids.forEach(function (id) {
  6680. var item = me.items[id];
  6681. if (item) {
  6682. count++;
  6683. me._removeItem(item);
  6684. }
  6685. });
  6686. if (count) {
  6687. // update order
  6688. this._order();
  6689. this.stackDirty = true; // force re-stacking of all items next redraw
  6690. this.body.emitter.emit('change');
  6691. }
  6692. };
  6693. /**
  6694. * Update the order of item in all groups
  6695. * @private
  6696. */
  6697. ItemSet.prototype._order = function() {
  6698. // reorder the items in all groups
  6699. // TODO: optimization: only reorder groups affected by the changed items
  6700. util.forEach(this.groups, function (group) {
  6701. group.order();
  6702. });
  6703. };
  6704. /**
  6705. * Handle updated groups
  6706. * @param {Number[]} ids
  6707. * @private
  6708. */
  6709. ItemSet.prototype._onUpdateGroups = function(ids) {
  6710. this._onAddGroups(ids);
  6711. };
  6712. /**
  6713. * Handle changed groups
  6714. * @param {Number[]} ids
  6715. * @private
  6716. */
  6717. ItemSet.prototype._onAddGroups = function(ids) {
  6718. var me = this;
  6719. ids.forEach(function (id) {
  6720. var groupData = me.groupsData.get(id);
  6721. var group = me.groups[id];
  6722. if (!group) {
  6723. // check for reserved ids
  6724. if (id == UNGROUPED) {
  6725. throw new Error('Illegal group id. ' + id + ' is a reserved id.');
  6726. }
  6727. var groupOptions = Object.create(me.options);
  6728. util.extend(groupOptions, {
  6729. height: null
  6730. });
  6731. group = new Group(id, groupData, me);
  6732. me.groups[id] = group;
  6733. // add items with this groupId to the new group
  6734. for (var itemId in me.items) {
  6735. if (me.items.hasOwnProperty(itemId)) {
  6736. var item = me.items[itemId];
  6737. if (item.data.group == id) {
  6738. group.add(item);
  6739. }
  6740. }
  6741. }
  6742. group.order();
  6743. group.show();
  6744. }
  6745. else {
  6746. // update group
  6747. group.setData(groupData);
  6748. }
  6749. });
  6750. this.body.emitter.emit('change');
  6751. };
  6752. /**
  6753. * Handle removed groups
  6754. * @param {Number[]} ids
  6755. * @private
  6756. */
  6757. ItemSet.prototype._onRemoveGroups = function(ids) {
  6758. var groups = this.groups;
  6759. ids.forEach(function (id) {
  6760. var group = groups[id];
  6761. if (group) {
  6762. group.hide();
  6763. delete groups[id];
  6764. }
  6765. });
  6766. this.markDirty();
  6767. this.body.emitter.emit('change');
  6768. };
  6769. /**
  6770. * Reorder the groups if needed
  6771. * @return {boolean} changed
  6772. * @private
  6773. */
  6774. ItemSet.prototype._orderGroups = function () {
  6775. if (this.groupsData) {
  6776. // reorder the groups
  6777. var groupIds = this.groupsData.getIds({
  6778. order: this.options.groupOrder
  6779. });
  6780. var changed = !util.equalArray(groupIds, this.groupIds);
  6781. if (changed) {
  6782. // hide all groups, removes them from the DOM
  6783. var groups = this.groups;
  6784. groupIds.forEach(function (groupId) {
  6785. groups[groupId].hide();
  6786. });
  6787. // show the groups again, attach them to the DOM in correct order
  6788. groupIds.forEach(function (groupId) {
  6789. groups[groupId].show();
  6790. });
  6791. this.groupIds = groupIds;
  6792. }
  6793. return changed;
  6794. }
  6795. else {
  6796. return false;
  6797. }
  6798. };
  6799. /**
  6800. * Add a new item
  6801. * @param {Item} item
  6802. * @private
  6803. */
  6804. ItemSet.prototype._addItem = function(item) {
  6805. this.items[item.id] = item;
  6806. // add to group
  6807. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  6808. var group = this.groups[groupId];
  6809. if (group) group.add(item);
  6810. };
  6811. /**
  6812. * Update an existing item
  6813. * @param {Item} item
  6814. * @param {Object} itemData
  6815. * @private
  6816. */
  6817. ItemSet.prototype._updateItem = function(item, itemData) {
  6818. var oldGroupId = item.data.group;
  6819. item.data = itemData;
  6820. if (item.displayed) {
  6821. item.redraw();
  6822. }
  6823. // update group
  6824. if (oldGroupId != item.data.group) {
  6825. var oldGroup = this.groups[oldGroupId];
  6826. if (oldGroup) oldGroup.remove(item);
  6827. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  6828. var group = this.groups[groupId];
  6829. if (group) group.add(item);
  6830. }
  6831. };
  6832. /**
  6833. * Delete an item from the ItemSet: remove it from the DOM, from the map
  6834. * with items, and from the map with visible items, and from the selection
  6835. * @param {Item} item
  6836. * @private
  6837. */
  6838. ItemSet.prototype._removeItem = function(item) {
  6839. // remove from DOM
  6840. item.hide();
  6841. // remove from items
  6842. delete this.items[item.id];
  6843. // remove from selection
  6844. var index = this.selection.indexOf(item.id);
  6845. if (index != -1) this.selection.splice(index, 1);
  6846. // remove from group
  6847. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  6848. var group = this.groups[groupId];
  6849. if (group) group.remove(item);
  6850. };
  6851. /**
  6852. * Create an array containing all items being a range (having an end date)
  6853. * @param array
  6854. * @returns {Array}
  6855. * @private
  6856. */
  6857. ItemSet.prototype._constructByEndArray = function(array) {
  6858. var endArray = [];
  6859. for (var i = 0; i < array.length; i++) {
  6860. if (array[i] instanceof ItemRange) {
  6861. endArray.push(array[i]);
  6862. }
  6863. }
  6864. return endArray;
  6865. };
  6866. /**
  6867. * Register the clicked item on touch, before dragStart is initiated.
  6868. *
  6869. * dragStart is initiated from a mousemove event, which can have left the item
  6870. * already resulting in an item == null
  6871. *
  6872. * @param {Event} event
  6873. * @private
  6874. */
  6875. ItemSet.prototype._onTouch = function (event) {
  6876. // store the touched item, used in _onDragStart
  6877. this.touchParams.item = ItemSet.itemFromTarget(event);
  6878. };
  6879. /**
  6880. * Start dragging the selected events
  6881. * @param {Event} event
  6882. * @private
  6883. */
  6884. ItemSet.prototype._onDragStart = function (event) {
  6885. if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
  6886. return;
  6887. }
  6888. var item = this.touchParams.item || null,
  6889. me = this,
  6890. props;
  6891. if (item && item.selected) {
  6892. var dragLeftItem = event.target.dragLeftItem;
  6893. var dragRightItem = event.target.dragRightItem;
  6894. if (dragLeftItem) {
  6895. props = {
  6896. item: dragLeftItem
  6897. };
  6898. if (me.options.editable.updateTime) {
  6899. props.start = item.data.start.valueOf();
  6900. }
  6901. if (me.options.editable.updateGroup) {
  6902. if ('group' in item.data) props.group = item.data.group;
  6903. }
  6904. this.touchParams.itemProps = [props];
  6905. }
  6906. else if (dragRightItem) {
  6907. props = {
  6908. item: dragRightItem
  6909. };
  6910. if (me.options.editable.updateTime) {
  6911. props.end = item.data.end.valueOf();
  6912. }
  6913. if (me.options.editable.updateGroup) {
  6914. if ('group' in item.data) props.group = item.data.group;
  6915. }
  6916. this.touchParams.itemProps = [props];
  6917. }
  6918. else {
  6919. this.touchParams.itemProps = this.getSelection().map(function (id) {
  6920. var item = me.items[id];
  6921. var props = {
  6922. item: item
  6923. };
  6924. if (me.options.editable.updateTime) {
  6925. if ('start' in item.data) props.start = item.data.start.valueOf();
  6926. if ('end' in item.data) props.end = item.data.end.valueOf();
  6927. }
  6928. if (me.options.editable.updateGroup) {
  6929. if ('group' in item.data) props.group = item.data.group;
  6930. }
  6931. return props;
  6932. });
  6933. }
  6934. event.stopPropagation();
  6935. }
  6936. };
  6937. /**
  6938. * Drag selected items
  6939. * @param {Event} event
  6940. * @private
  6941. */
  6942. ItemSet.prototype._onDrag = function (event) {
  6943. if (this.touchParams.itemProps) {
  6944. var range = this.body.range,
  6945. snap = this.body.util.snap || null,
  6946. deltaX = event.gesture.deltaX,
  6947. scale = (this.props.width / (range.end - range.start)),
  6948. offset = deltaX / scale;
  6949. // move
  6950. this.touchParams.itemProps.forEach(function (props) {
  6951. if ('start' in props) {
  6952. var start = new Date(props.start + offset);
  6953. props.item.data.start = snap ? snap(start) : start;
  6954. }
  6955. if ('end' in props) {
  6956. var end = new Date(props.end + offset);
  6957. props.item.data.end = snap ? snap(end) : end;
  6958. }
  6959. if ('group' in props) {
  6960. // drag from one group to another
  6961. var group = ItemSet.groupFromTarget(event);
  6962. if (group && group.groupId != props.item.data.group) {
  6963. var oldGroup = props.item.parent;
  6964. oldGroup.remove(props.item);
  6965. oldGroup.order();
  6966. group.add(props.item);
  6967. group.order();
  6968. props.item.data.group = group.groupId;
  6969. }
  6970. }
  6971. });
  6972. // TODO: implement onMoving handler
  6973. this.stackDirty = true; // force re-stacking of all items next redraw
  6974. this.body.emitter.emit('change');
  6975. event.stopPropagation();
  6976. }
  6977. };
  6978. /**
  6979. * End of dragging selected items
  6980. * @param {Event} event
  6981. * @private
  6982. */
  6983. ItemSet.prototype._onDragEnd = function (event) {
  6984. if (this.touchParams.itemProps) {
  6985. // prepare a change set for the changed items
  6986. var changes = [],
  6987. me = this,
  6988. dataset = this._myDataSet();
  6989. this.touchParams.itemProps.forEach(function (props) {
  6990. var id = props.item.id,
  6991. itemData = me.itemsData.get(id, me.itemOptions);
  6992. var changed = false;
  6993. if ('start' in props.item.data) {
  6994. changed = (props.start != props.item.data.start.valueOf());
  6995. itemData.start = util.convert(props.item.data.start,
  6996. dataset._options.type && dataset._options.type.start || 'Date');
  6997. }
  6998. if ('end' in props.item.data) {
  6999. changed = changed || (props.end != props.item.data.end.valueOf());
  7000. itemData.end = util.convert(props.item.data.end,
  7001. dataset._options.type && dataset._options.type.end || 'Date');
  7002. }
  7003. if ('group' in props.item.data) {
  7004. changed = changed || (props.group != props.item.data.group);
  7005. itemData.group = props.item.data.group;
  7006. }
  7007. // only apply changes when start or end is actually changed
  7008. if (changed) {
  7009. me.options.onMove(itemData, function (itemData) {
  7010. if (itemData) {
  7011. // apply changes
  7012. itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
  7013. changes.push(itemData);
  7014. }
  7015. else {
  7016. // restore original values
  7017. if ('start' in props) props.item.data.start = props.start;
  7018. if ('end' in props) props.item.data.end = props.end;
  7019. me.stackDirty = true; // force re-stacking of all items next redraw
  7020. me.body.emitter.emit('change');
  7021. }
  7022. });
  7023. }
  7024. });
  7025. this.touchParams.itemProps = null;
  7026. // apply the changes to the data (if there are changes)
  7027. if (changes.length) {
  7028. dataset.update(changes);
  7029. }
  7030. event.stopPropagation();
  7031. }
  7032. };
  7033. /**
  7034. * Handle selecting/deselecting an item when tapping it
  7035. * @param {Event} event
  7036. * @private
  7037. */
  7038. ItemSet.prototype._onSelectItem = function (event) {
  7039. if (!this.options.selectable) return;
  7040. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  7041. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  7042. if (ctrlKey || shiftKey) {
  7043. this._onMultiSelectItem(event);
  7044. return;
  7045. }
  7046. var oldSelection = this.getSelection();
  7047. var item = ItemSet.itemFromTarget(event);
  7048. var selection = item ? [item.id] : [];
  7049. this.setSelection(selection);
  7050. var newSelection = this.getSelection();
  7051. // emit a select event,
  7052. // except when old selection is empty and new selection is still empty
  7053. if (newSelection.length > 0 || oldSelection.length > 0) {
  7054. this.body.emitter.emit('select', {
  7055. items: this.getSelection()
  7056. });
  7057. }
  7058. event.stopPropagation();
  7059. };
  7060. /**
  7061. * Handle creation and updates of an item on double tap
  7062. * @param event
  7063. * @private
  7064. */
  7065. ItemSet.prototype._onAddItem = function (event) {
  7066. if (!this.options.selectable) return;
  7067. if (!this.options.editable.add) return;
  7068. var me = this,
  7069. snap = this.body.util.snap || null,
  7070. item = ItemSet.itemFromTarget(event);
  7071. if (item) {
  7072. // update item
  7073. // execute async handler to update the item (or cancel it)
  7074. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  7075. this.options.onUpdate(itemData, function (itemData) {
  7076. if (itemData) {
  7077. me.itemsData.update(itemData);
  7078. }
  7079. });
  7080. }
  7081. else {
  7082. // add item
  7083. var xAbs = vis.util.getAbsoluteLeft(this.dom.frame);
  7084. var x = event.gesture.center.pageX - xAbs;
  7085. var start = this.body.util.toTime(x);
  7086. var newItem = {
  7087. start: snap ? snap(start) : start,
  7088. content: 'new item'
  7089. };
  7090. // when default type is a range, add a default end date to the new item
  7091. if (this.options.type === 'range') {
  7092. var end = this.body.util.toTime(x + this.props.width / 5);
  7093. newItem.end = snap ? snap(end) : end;
  7094. }
  7095. newItem[this.itemsData.fieldId] = util.randomUUID();
  7096. var group = ItemSet.groupFromTarget(event);
  7097. if (group) {
  7098. newItem.group = group.groupId;
  7099. }
  7100. // execute async handler to customize (or cancel) adding an item
  7101. this.options.onAdd(newItem, function (item) {
  7102. if (item) {
  7103. me.itemsData.add(newItem);
  7104. // TODO: need to trigger a redraw?
  7105. }
  7106. });
  7107. }
  7108. };
  7109. /**
  7110. * Handle selecting/deselecting multiple items when holding an item
  7111. * @param {Event} event
  7112. * @private
  7113. */
  7114. ItemSet.prototype._onMultiSelectItem = function (event) {
  7115. if (!this.options.selectable) return;
  7116. var selection,
  7117. item = ItemSet.itemFromTarget(event);
  7118. if (item) {
  7119. // multi select items
  7120. selection = this.getSelection(); // current selection
  7121. var index = selection.indexOf(item.id);
  7122. if (index == -1) {
  7123. // item is not yet selected -> select it
  7124. selection.push(item.id);
  7125. }
  7126. else {
  7127. // item is already selected -> deselect it
  7128. selection.splice(index, 1);
  7129. }
  7130. this.setSelection(selection);
  7131. this.body.emitter.emit('select', {
  7132. items: this.getSelection()
  7133. });
  7134. event.stopPropagation();
  7135. }
  7136. };
  7137. /**
  7138. * Find an item from an event target:
  7139. * searches for the attribute 'timeline-item' in the event target's element tree
  7140. * @param {Event} event
  7141. * @return {Item | null} item
  7142. */
  7143. ItemSet.itemFromTarget = function(event) {
  7144. var target = event.target;
  7145. while (target) {
  7146. if (target.hasOwnProperty('timeline-item')) {
  7147. return target['timeline-item'];
  7148. }
  7149. target = target.parentNode;
  7150. }
  7151. return null;
  7152. };
  7153. /**
  7154. * Find the Group from an event target:
  7155. * searches for the attribute 'timeline-group' in the event target's element tree
  7156. * @param {Event} event
  7157. * @return {Group | null} group
  7158. */
  7159. ItemSet.groupFromTarget = function(event) {
  7160. var target = event.target;
  7161. while (target) {
  7162. if (target.hasOwnProperty('timeline-group')) {
  7163. return target['timeline-group'];
  7164. }
  7165. target = target.parentNode;
  7166. }
  7167. return null;
  7168. };
  7169. /**
  7170. * Find the ItemSet from an event target:
  7171. * searches for the attribute 'timeline-itemset' in the event target's element tree
  7172. * @param {Event} event
  7173. * @return {ItemSet | null} item
  7174. */
  7175. ItemSet.itemSetFromTarget = function(event) {
  7176. var target = event.target;
  7177. while (target) {
  7178. if (target.hasOwnProperty('timeline-itemset')) {
  7179. return target['timeline-itemset'];
  7180. }
  7181. target = target.parentNode;
  7182. }
  7183. return null;
  7184. };
  7185. /**
  7186. * Find the DataSet to which this ItemSet is connected
  7187. * @returns {null | DataSet} dataset
  7188. * @private
  7189. */
  7190. ItemSet.prototype._myDataSet = function() {
  7191. // find the root DataSet
  7192. var dataset = this.itemsData;
  7193. while (dataset instanceof DataView) {
  7194. dataset = dataset.data;
  7195. }
  7196. return dataset;
  7197. };
  7198. /**
  7199. * @constructor Item
  7200. * @param {Object} data Object containing (optional) parameters type,
  7201. * start, end, content, group, className.
  7202. * @param {{toScreen: function, toTime: function}} conversion
  7203. * Conversion functions from time to screen and vice versa
  7204. * @param {Object} options Configuration options
  7205. * // TODO: describe available options
  7206. */
  7207. function Item (data, conversion, options) {
  7208. this.id = null;
  7209. this.parent = null;
  7210. this.data = data;
  7211. this.dom = null;
  7212. this.conversion = conversion || {};
  7213. this.options = options || {};
  7214. this.selected = false;
  7215. this.displayed = false;
  7216. this.dirty = true;
  7217. this.top = null;
  7218. this.left = null;
  7219. this.width = null;
  7220. this.height = null;
  7221. }
  7222. /**
  7223. * Select current item
  7224. */
  7225. Item.prototype.select = function() {
  7226. this.selected = true;
  7227. if (this.displayed) this.redraw();
  7228. };
  7229. /**
  7230. * Unselect current item
  7231. */
  7232. Item.prototype.unselect = function() {
  7233. this.selected = false;
  7234. if (this.displayed) this.redraw();
  7235. };
  7236. /**
  7237. * Set a parent for the item
  7238. * @param {ItemSet | Group} parent
  7239. */
  7240. Item.prototype.setParent = function(parent) {
  7241. if (this.displayed) {
  7242. this.hide();
  7243. this.parent = parent;
  7244. if (this.parent) {
  7245. this.show();
  7246. }
  7247. }
  7248. else {
  7249. this.parent = parent;
  7250. }
  7251. };
  7252. /**
  7253. * Check whether this item is visible inside given range
  7254. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7255. * @returns {boolean} True if visible
  7256. */
  7257. Item.prototype.isVisible = function(range) {
  7258. // Should be implemented by Item implementations
  7259. return false;
  7260. };
  7261. /**
  7262. * Show the Item in the DOM (when not already visible)
  7263. * @return {Boolean} changed
  7264. */
  7265. Item.prototype.show = function() {
  7266. return false;
  7267. };
  7268. /**
  7269. * Hide the Item from the DOM (when visible)
  7270. * @return {Boolean} changed
  7271. */
  7272. Item.prototype.hide = function() {
  7273. return false;
  7274. };
  7275. /**
  7276. * Repaint the item
  7277. */
  7278. Item.prototype.redraw = function() {
  7279. // should be implemented by the item
  7280. };
  7281. /**
  7282. * Reposition the Item horizontally
  7283. */
  7284. Item.prototype.repositionX = function() {
  7285. // should be implemented by the item
  7286. };
  7287. /**
  7288. * Reposition the Item vertically
  7289. */
  7290. Item.prototype.repositionY = function() {
  7291. // should be implemented by the item
  7292. };
  7293. /**
  7294. * Repaint a delete button on the top right of the item when the item is selected
  7295. * @param {HTMLElement} anchor
  7296. * @protected
  7297. */
  7298. Item.prototype._repaintDeleteButton = function (anchor) {
  7299. if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
  7300. // create and show button
  7301. var me = this;
  7302. var deleteButton = document.createElement('div');
  7303. deleteButton.className = 'delete';
  7304. deleteButton.title = 'Delete this item';
  7305. Hammer(deleteButton, {
  7306. preventDefault: true
  7307. }).on('tap', function (event) {
  7308. me.parent.removeFromDataSet(me);
  7309. event.stopPropagation();
  7310. });
  7311. anchor.appendChild(deleteButton);
  7312. this.dom.deleteButton = deleteButton;
  7313. }
  7314. else if (!this.selected && this.dom.deleteButton) {
  7315. // remove button
  7316. if (this.dom.deleteButton.parentNode) {
  7317. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  7318. }
  7319. this.dom.deleteButton = null;
  7320. }
  7321. };
  7322. /**
  7323. * @constructor ItemBox
  7324. * @extends Item
  7325. * @param {Object} data Object containing parameters start
  7326. * content, className.
  7327. * @param {{toScreen: function, toTime: function}} conversion
  7328. * Conversion functions from time to screen and vice versa
  7329. * @param {Object} [options] Configuration options
  7330. * // TODO: describe available options
  7331. */
  7332. function ItemBox (data, conversion, options) {
  7333. this.props = {
  7334. dot: {
  7335. width: 0,
  7336. height: 0
  7337. },
  7338. line: {
  7339. width: 0,
  7340. height: 0
  7341. }
  7342. };
  7343. // validate data
  7344. if (data) {
  7345. if (data.start == undefined) {
  7346. throw new Error('Property "start" missing in item ' + data);
  7347. }
  7348. }
  7349. Item.call(this, data, conversion, options);
  7350. }
  7351. ItemBox.prototype = new Item (null, null, null);
  7352. /**
  7353. * Check whether this item is visible inside given range
  7354. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7355. * @returns {boolean} True if visible
  7356. */
  7357. ItemBox.prototype.isVisible = function(range) {
  7358. // determine visibility
  7359. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  7360. var interval = (range.end - range.start) / 4;
  7361. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  7362. };
  7363. /**
  7364. * Repaint the item
  7365. */
  7366. ItemBox.prototype.redraw = function() {
  7367. var dom = this.dom;
  7368. if (!dom) {
  7369. // create DOM
  7370. this.dom = {};
  7371. dom = this.dom;
  7372. // create main box
  7373. dom.box = document.createElement('DIV');
  7374. // contents box (inside the background box). used for making margins
  7375. dom.content = document.createElement('DIV');
  7376. dom.content.className = 'content';
  7377. dom.box.appendChild(dom.content);
  7378. // line to axis
  7379. dom.line = document.createElement('DIV');
  7380. dom.line.className = 'line';
  7381. // dot on axis
  7382. dom.dot = document.createElement('DIV');
  7383. dom.dot.className = 'dot';
  7384. // attach this item as attribute
  7385. dom.box['timeline-item'] = this;
  7386. }
  7387. // append DOM to parent DOM
  7388. if (!this.parent) {
  7389. throw new Error('Cannot redraw item: no parent attached');
  7390. }
  7391. if (!dom.box.parentNode) {
  7392. var foreground = this.parent.dom.foreground;
  7393. if (!foreground) throw new Error('Cannot redraw time axis: parent has no foreground container element');
  7394. foreground.appendChild(dom.box);
  7395. }
  7396. if (!dom.line.parentNode) {
  7397. var background = this.parent.dom.background;
  7398. if (!background) throw new Error('Cannot redraw time axis: parent has no background container element');
  7399. background.appendChild(dom.line);
  7400. }
  7401. if (!dom.dot.parentNode) {
  7402. var axis = this.parent.dom.axis;
  7403. if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
  7404. axis.appendChild(dom.dot);
  7405. }
  7406. this.displayed = true;
  7407. // update contents
  7408. if (this.data.content != this.content) {
  7409. this.content = this.data.content;
  7410. if (this.content instanceof Element) {
  7411. dom.content.innerHTML = '';
  7412. dom.content.appendChild(this.content);
  7413. }
  7414. else if (this.data.content != undefined) {
  7415. dom.content.innerHTML = this.content;
  7416. }
  7417. else {
  7418. throw new Error('Property "content" missing in item ' + this.data.id);
  7419. }
  7420. this.dirty = true;
  7421. }
  7422. // update class
  7423. var className = (this.data.className? ' ' + this.data.className : '') +
  7424. (this.selected ? ' selected' : '');
  7425. if (this.className != className) {
  7426. this.className = className;
  7427. dom.box.className = 'item box' + className;
  7428. dom.line.className = 'item line' + className;
  7429. dom.dot.className = 'item dot' + className;
  7430. this.dirty = true;
  7431. }
  7432. // recalculate size
  7433. if (this.dirty) {
  7434. this.props.dot.height = dom.dot.offsetHeight;
  7435. this.props.dot.width = dom.dot.offsetWidth;
  7436. this.props.line.width = dom.line.offsetWidth;
  7437. this.width = dom.box.offsetWidth;
  7438. this.height = dom.box.offsetHeight;
  7439. this.dirty = false;
  7440. }
  7441. this._repaintDeleteButton(dom.box);
  7442. };
  7443. /**
  7444. * Show the item in the DOM (when not already displayed). The items DOM will
  7445. * be created when needed.
  7446. */
  7447. ItemBox.prototype.show = function() {
  7448. if (!this.displayed) {
  7449. this.redraw();
  7450. }
  7451. };
  7452. /**
  7453. * Hide the item from the DOM (when visible)
  7454. */
  7455. ItemBox.prototype.hide = function() {
  7456. if (this.displayed) {
  7457. var dom = this.dom;
  7458. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  7459. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  7460. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  7461. this.top = null;
  7462. this.left = null;
  7463. this.displayed = false;
  7464. }
  7465. };
  7466. /**
  7467. * Reposition the item horizontally
  7468. * @Override
  7469. */
  7470. ItemBox.prototype.repositionX = function() {
  7471. var start = this.conversion.toScreen(this.data.start),
  7472. align = this.options.align,
  7473. left,
  7474. box = this.dom.box,
  7475. line = this.dom.line,
  7476. dot = this.dom.dot;
  7477. // calculate left position of the box
  7478. if (align == 'right') {
  7479. this.left = start - this.width;
  7480. }
  7481. else if (align == 'left') {
  7482. this.left = start;
  7483. }
  7484. else {
  7485. // default or 'center'
  7486. this.left = start - this.width / 2;
  7487. }
  7488. // reposition box
  7489. box.style.left = this.left + 'px';
  7490. // reposition line
  7491. line.style.left = (start - this.props.line.width / 2) + 'px';
  7492. // reposition dot
  7493. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  7494. };
  7495. /**
  7496. * Reposition the item vertically
  7497. * @Override
  7498. */
  7499. ItemBox.prototype.repositionY = function() {
  7500. var orientation = this.options.orientation,
  7501. box = this.dom.box,
  7502. line = this.dom.line,
  7503. dot = this.dom.dot;
  7504. if (orientation == 'top') {
  7505. box.style.top = (this.top || 0) + 'px';
  7506. line.style.top = '0';
  7507. line.style.height = (this.parent.top + this.top + 1) + 'px';
  7508. line.style.bottom = '';
  7509. }
  7510. else { // orientation 'bottom'
  7511. var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
  7512. var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
  7513. box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
  7514. line.style.top = (itemSetHeight - lineHeight) + 'px';
  7515. line.style.bottom = '0';
  7516. }
  7517. dot.style.top = (-this.props.dot.height / 2) + 'px';
  7518. };
  7519. /**
  7520. * @constructor ItemPoint
  7521. * @extends Item
  7522. * @param {Object} data Object containing parameters start
  7523. * content, className.
  7524. * @param {{toScreen: function, toTime: function}} conversion
  7525. * Conversion functions from time to screen and vice versa
  7526. * @param {Object} [options] Configuration options
  7527. * // TODO: describe available options
  7528. */
  7529. function ItemPoint (data, conversion, options) {
  7530. this.props = {
  7531. dot: {
  7532. top: 0,
  7533. width: 0,
  7534. height: 0
  7535. },
  7536. content: {
  7537. height: 0,
  7538. marginLeft: 0
  7539. }
  7540. };
  7541. // validate data
  7542. if (data) {
  7543. if (data.start == undefined) {
  7544. throw new Error('Property "start" missing in item ' + data);
  7545. }
  7546. }
  7547. Item.call(this, data, conversion, options);
  7548. }
  7549. ItemPoint.prototype = new Item (null, null, null);
  7550. /**
  7551. * Check whether this item is visible inside given range
  7552. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7553. * @returns {boolean} True if visible
  7554. */
  7555. ItemPoint.prototype.isVisible = function(range) {
  7556. // determine visibility
  7557. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  7558. var interval = (range.end - range.start) / 4;
  7559. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  7560. };
  7561. /**
  7562. * Repaint the item
  7563. */
  7564. ItemPoint.prototype.redraw = function() {
  7565. var dom = this.dom;
  7566. if (!dom) {
  7567. // create DOM
  7568. this.dom = {};
  7569. dom = this.dom;
  7570. // background box
  7571. dom.point = document.createElement('div');
  7572. // className is updated in redraw()
  7573. // contents box, right from the dot
  7574. dom.content = document.createElement('div');
  7575. dom.content.className = 'content';
  7576. dom.point.appendChild(dom.content);
  7577. // dot at start
  7578. dom.dot = document.createElement('div');
  7579. dom.point.appendChild(dom.dot);
  7580. // attach this item as attribute
  7581. dom.point['timeline-item'] = this;
  7582. }
  7583. // append DOM to parent DOM
  7584. if (!this.parent) {
  7585. throw new Error('Cannot redraw item: no parent attached');
  7586. }
  7587. if (!dom.point.parentNode) {
  7588. var foreground = this.parent.dom.foreground;
  7589. if (!foreground) {
  7590. throw new Error('Cannot redraw time axis: parent has no foreground container element');
  7591. }
  7592. foreground.appendChild(dom.point);
  7593. }
  7594. this.displayed = true;
  7595. // update contents
  7596. if (this.data.content != this.content) {
  7597. this.content = this.data.content;
  7598. if (this.content instanceof Element) {
  7599. dom.content.innerHTML = '';
  7600. dom.content.appendChild(this.content);
  7601. }
  7602. else if (this.data.content != undefined) {
  7603. dom.content.innerHTML = this.content;
  7604. }
  7605. else {
  7606. throw new Error('Property "content" missing in item ' + this.data.id);
  7607. }
  7608. this.dirty = true;
  7609. }
  7610. // update class
  7611. var className = (this.data.className? ' ' + this.data.className : '') +
  7612. (this.selected ? ' selected' : '');
  7613. if (this.className != className) {
  7614. this.className = className;
  7615. dom.point.className = 'item point' + className;
  7616. dom.dot.className = 'item dot' + className;
  7617. this.dirty = true;
  7618. }
  7619. // recalculate size
  7620. if (this.dirty) {
  7621. this.width = dom.point.offsetWidth;
  7622. this.height = dom.point.offsetHeight;
  7623. this.props.dot.width = dom.dot.offsetWidth;
  7624. this.props.dot.height = dom.dot.offsetHeight;
  7625. this.props.content.height = dom.content.offsetHeight;
  7626. // resize contents
  7627. dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
  7628. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  7629. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  7630. dom.dot.style.left = (this.props.dot.width / 2) + 'px';
  7631. this.dirty = false;
  7632. }
  7633. this._repaintDeleteButton(dom.point);
  7634. };
  7635. /**
  7636. * Show the item in the DOM (when not already visible). The items DOM will
  7637. * be created when needed.
  7638. */
  7639. ItemPoint.prototype.show = function() {
  7640. if (!this.displayed) {
  7641. this.redraw();
  7642. }
  7643. };
  7644. /**
  7645. * Hide the item from the DOM (when visible)
  7646. */
  7647. ItemPoint.prototype.hide = function() {
  7648. if (this.displayed) {
  7649. if (this.dom.point.parentNode) {
  7650. this.dom.point.parentNode.removeChild(this.dom.point);
  7651. }
  7652. this.top = null;
  7653. this.left = null;
  7654. this.displayed = false;
  7655. }
  7656. };
  7657. /**
  7658. * Reposition the item horizontally
  7659. * @Override
  7660. */
  7661. ItemPoint.prototype.repositionX = function() {
  7662. var start = this.conversion.toScreen(this.data.start);
  7663. this.left = start - this.props.dot.width;
  7664. // reposition point
  7665. this.dom.point.style.left = this.left + 'px';
  7666. };
  7667. /**
  7668. * Reposition the item vertically
  7669. * @Override
  7670. */
  7671. ItemPoint.prototype.repositionY = function() {
  7672. var orientation = this.options.orientation,
  7673. point = this.dom.point;
  7674. if (orientation == 'top') {
  7675. point.style.top = this.top + 'px';
  7676. }
  7677. else {
  7678. point.style.top = (this.parent.height - this.top - this.height) + 'px';
  7679. }
  7680. };
  7681. /**
  7682. * @constructor ItemRange
  7683. * @extends Item
  7684. * @param {Object} data Object containing parameters start, end
  7685. * content, className.
  7686. * @param {{toScreen: function, toTime: function}} conversion
  7687. * Conversion functions from time to screen and vice versa
  7688. * @param {Object} [options] Configuration options
  7689. * // TODO: describe options
  7690. */
  7691. function ItemRange (data, conversion, options) {
  7692. this.props = {
  7693. content: {
  7694. width: 0
  7695. }
  7696. };
  7697. this.overflow = false; // if contents can overflow (css styling), this flag is set to true
  7698. // validate data
  7699. if (data) {
  7700. if (data.start == undefined) {
  7701. throw new Error('Property "start" missing in item ' + data.id);
  7702. }
  7703. if (data.end == undefined) {
  7704. throw new Error('Property "end" missing in item ' + data.id);
  7705. }
  7706. }
  7707. Item.call(this, data, conversion, options);
  7708. }
  7709. ItemRange.prototype = new Item (null, null, null);
  7710. ItemRange.prototype.baseClassName = 'item range';
  7711. /**
  7712. * Check whether this item is visible inside given range
  7713. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7714. * @returns {boolean} True if visible
  7715. */
  7716. ItemRange.prototype.isVisible = function(range) {
  7717. // determine visibility
  7718. return (this.data.start < range.end) && (this.data.end > range.start);
  7719. };
  7720. /**
  7721. * Repaint the item
  7722. */
  7723. ItemRange.prototype.redraw = function() {
  7724. var dom = this.dom;
  7725. if (!dom) {
  7726. // create DOM
  7727. this.dom = {};
  7728. dom = this.dom;
  7729. // background box
  7730. dom.box = document.createElement('div');
  7731. // className is updated in redraw()
  7732. // contents box
  7733. dom.content = document.createElement('div');
  7734. dom.content.className = 'content';
  7735. dom.box.appendChild(dom.content);
  7736. // attach this item as attribute
  7737. dom.box['timeline-item'] = this;
  7738. }
  7739. // append DOM to parent DOM
  7740. if (!this.parent) {
  7741. throw new Error('Cannot redraw item: no parent attached');
  7742. }
  7743. if (!dom.box.parentNode) {
  7744. var foreground = this.parent.dom.foreground;
  7745. if (!foreground) {
  7746. throw new Error('Cannot redraw time axis: parent has no foreground container element');
  7747. }
  7748. foreground.appendChild(dom.box);
  7749. }
  7750. this.displayed = true;
  7751. // update contents
  7752. if (this.data.content != this.content) {
  7753. this.content = this.data.content;
  7754. if (this.content instanceof Element) {
  7755. dom.content.innerHTML = '';
  7756. dom.content.appendChild(this.content);
  7757. }
  7758. else if (this.data.content != undefined) {
  7759. dom.content.innerHTML = this.content;
  7760. }
  7761. else {
  7762. throw new Error('Property "content" missing in item ' + this.data.id);
  7763. }
  7764. this.dirty = true;
  7765. }
  7766. // update class
  7767. var className = (this.data.className ? (' ' + this.data.className) : '') +
  7768. (this.selected ? ' selected' : '');
  7769. if (this.className != className) {
  7770. this.className = className;
  7771. dom.box.className = this.baseClassName + className;
  7772. this.dirty = true;
  7773. }
  7774. // recalculate size
  7775. if (this.dirty) {
  7776. // determine from css whether this box has overflow
  7777. this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
  7778. this.props.content.width = this.dom.content.offsetWidth;
  7779. this.height = this.dom.box.offsetHeight;
  7780. this.dirty = false;
  7781. }
  7782. this._repaintDeleteButton(dom.box);
  7783. this._repaintDragLeft();
  7784. this._repaintDragRight();
  7785. };
  7786. /**
  7787. * Show the item in the DOM (when not already visible). The items DOM will
  7788. * be created when needed.
  7789. */
  7790. ItemRange.prototype.show = function() {
  7791. if (!this.displayed) {
  7792. this.redraw();
  7793. }
  7794. };
  7795. /**
  7796. * Hide the item from the DOM (when visible)
  7797. * @return {Boolean} changed
  7798. */
  7799. ItemRange.prototype.hide = function() {
  7800. if (this.displayed) {
  7801. var box = this.dom.box;
  7802. if (box.parentNode) {
  7803. box.parentNode.removeChild(box);
  7804. }
  7805. this.top = null;
  7806. this.left = null;
  7807. this.displayed = false;
  7808. }
  7809. };
  7810. /**
  7811. * Reposition the item horizontally
  7812. * @Override
  7813. */
  7814. // TODO: delete the old function
  7815. ItemRange.prototype.repositionX = function() {
  7816. var props = this.props,
  7817. parentWidth = this.parent.width,
  7818. start = this.conversion.toScreen(this.data.start),
  7819. end = this.conversion.toScreen(this.data.end),
  7820. padding = this.options.padding,
  7821. contentLeft;
  7822. // limit the width of the this, as browsers cannot draw very wide divs
  7823. if (start < -parentWidth) {
  7824. start = -parentWidth;
  7825. }
  7826. if (end > 2 * parentWidth) {
  7827. end = 2 * parentWidth;
  7828. }
  7829. var boxWidth = Math.max(end - start, 1);
  7830. if (this.overflow) {
  7831. // when range exceeds left of the window, position the contents at the left of the visible area
  7832. contentLeft = Math.max(-start, 0);
  7833. this.left = start;
  7834. this.width = boxWidth + this.props.content.width;
  7835. // Note: The calculation of width is an optimistic calculation, giving
  7836. // a width which will not change when moving the Timeline
  7837. // So no restacking needed, which is nicer for the eye;
  7838. }
  7839. else { // no overflow
  7840. // when range exceeds left of the window, position the contents at the left of the visible area
  7841. if (start < 0) {
  7842. contentLeft = Math.min(-start,
  7843. (end - start - props.content.width - 2 * padding));
  7844. // TODO: remove the need for options.padding. it's terrible.
  7845. }
  7846. else {
  7847. contentLeft = 0;
  7848. }
  7849. this.left = start;
  7850. this.width = boxWidth;
  7851. }
  7852. this.dom.box.style.left = this.left + 'px';
  7853. this.dom.box.style.width = boxWidth + 'px';
  7854. this.dom.content.style.left = contentLeft + 'px';
  7855. };
  7856. /**
  7857. * Reposition the item vertically
  7858. * @Override
  7859. */
  7860. ItemRange.prototype.repositionY = function() {
  7861. var orientation = this.options.orientation,
  7862. box = this.dom.box;
  7863. if (orientation == 'top') {
  7864. box.style.top = this.top + 'px';
  7865. }
  7866. else {
  7867. box.style.top = (this.parent.height - this.top - this.height) + 'px';
  7868. }
  7869. };
  7870. /**
  7871. * Repaint a drag area on the left side of the range when the range is selected
  7872. * @protected
  7873. */
  7874. ItemRange.prototype._repaintDragLeft = function () {
  7875. if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
  7876. // create and show drag area
  7877. var dragLeft = document.createElement('div');
  7878. dragLeft.className = 'drag-left';
  7879. dragLeft.dragLeftItem = this;
  7880. // TODO: this should be redundant?
  7881. Hammer(dragLeft, {
  7882. preventDefault: true
  7883. }).on('drag', function () {
  7884. //console.log('drag left')
  7885. });
  7886. this.dom.box.appendChild(dragLeft);
  7887. this.dom.dragLeft = dragLeft;
  7888. }
  7889. else if (!this.selected && this.dom.dragLeft) {
  7890. // delete drag area
  7891. if (this.dom.dragLeft.parentNode) {
  7892. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  7893. }
  7894. this.dom.dragLeft = null;
  7895. }
  7896. };
  7897. /**
  7898. * Repaint a drag area on the right side of the range when the range is selected
  7899. * @protected
  7900. */
  7901. ItemRange.prototype._repaintDragRight = function () {
  7902. if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
  7903. // create and show drag area
  7904. var dragRight = document.createElement('div');
  7905. dragRight.className = 'drag-right';
  7906. dragRight.dragRightItem = this;
  7907. // TODO: this should be redundant?
  7908. Hammer(dragRight, {
  7909. preventDefault: true
  7910. }).on('drag', function () {
  7911. //console.log('drag right')
  7912. });
  7913. this.dom.box.appendChild(dragRight);
  7914. this.dom.dragRight = dragRight;
  7915. }
  7916. else if (!this.selected && this.dom.dragRight) {
  7917. // delete drag area
  7918. if (this.dom.dragRight.parentNode) {
  7919. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  7920. }
  7921. this.dom.dragRight = null;
  7922. }
  7923. };
  7924. /**
  7925. * @constructor Group
  7926. * @param {Number | String} groupId
  7927. * @param {Object} data
  7928. * @param {ItemSet} itemSet
  7929. */
  7930. function Group (groupId, data, itemSet) {
  7931. this.groupId = groupId;
  7932. this.itemSet = itemSet;
  7933. this.dom = {};
  7934. this.props = {
  7935. label: {
  7936. width: 0,
  7937. height: 0
  7938. }
  7939. };
  7940. this.className = null;
  7941. this.items = {}; // items filtered by groupId of this group
  7942. this.visibleItems = []; // items currently visible in window
  7943. this.orderedItems = { // items sorted by start and by end
  7944. byStart: [],
  7945. byEnd: []
  7946. };
  7947. this._create();
  7948. this.setData(data);
  7949. }
  7950. /**
  7951. * Create DOM elements for the group
  7952. * @private
  7953. */
  7954. Group.prototype._create = function() {
  7955. var label = document.createElement('div');
  7956. label.className = 'vlabel';
  7957. this.dom.label = label;
  7958. var inner = document.createElement('div');
  7959. inner.className = 'inner';
  7960. label.appendChild(inner);
  7961. this.dom.inner = inner;
  7962. var foreground = document.createElement('div');
  7963. foreground.className = 'group';
  7964. foreground['timeline-group'] = this;
  7965. this.dom.foreground = foreground;
  7966. this.dom.background = document.createElement('div');
  7967. this.dom.background.className = 'group';
  7968. this.dom.axis = document.createElement('div');
  7969. this.dom.axis.className = 'group';
  7970. // create a hidden marker to detect when the Timelines container is attached
  7971. // to the DOM, or the style of a parent of the Timeline is changed from
  7972. // display:none is changed to visible.
  7973. this.dom.marker = document.createElement('div');
  7974. this.dom.marker.style.visibility = 'hidden';
  7975. this.dom.marker.innerHTML = '?';
  7976. this.dom.background.appendChild(this.dom.marker);
  7977. };
  7978. /**
  7979. * Set the group data for this group
  7980. * @param {Object} data Group data, can contain properties content and className
  7981. */
  7982. Group.prototype.setData = function(data) {
  7983. // update contents
  7984. var content = data && data.content;
  7985. if (content instanceof Element) {
  7986. this.dom.inner.appendChild(content);
  7987. }
  7988. else if (content != undefined) {
  7989. this.dom.inner.innerHTML = content;
  7990. }
  7991. else {
  7992. this.dom.inner.innerHTML = this.groupId;
  7993. }
  7994. if (!this.dom.inner.firstChild) {
  7995. util.addClassName(this.dom.inner, 'hidden');
  7996. }
  7997. else {
  7998. util.removeClassName(this.dom.inner, 'hidden');
  7999. }
  8000. // update className
  8001. var className = data && data.className || null;
  8002. if (className != this.className) {
  8003. if (this.className) {
  8004. util.removeClassName(this.dom.label, className);
  8005. util.removeClassName(this.dom.foreground, className);
  8006. util.removeClassName(this.dom.background, className);
  8007. util.removeClassName(this.dom.axis, className);
  8008. }
  8009. util.addClassName(this.dom.label, className);
  8010. util.addClassName(this.dom.foreground, className);
  8011. util.addClassName(this.dom.background, className);
  8012. util.addClassName(this.dom.axis, className);
  8013. }
  8014. };
  8015. /**
  8016. * Get the width of the group label
  8017. * @return {number} width
  8018. */
  8019. Group.prototype.getLabelWidth = function() {
  8020. return this.props.label.width;
  8021. };
  8022. /**
  8023. * Repaint this group
  8024. * @param {{start: number, end: number}} range
  8025. * @param {{item: number, axis: number}} margin
  8026. * @param {boolean} [restack=false] Force restacking of all items
  8027. * @return {boolean} Returns true if the group is resized
  8028. */
  8029. Group.prototype.redraw = function(range, margin, restack) {
  8030. var resized = false;
  8031. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  8032. // force recalculation of the height of the items when the marker height changed
  8033. // (due to the Timeline being attached to the DOM or changed from display:none to visible)
  8034. var markerHeight = this.dom.marker.clientHeight;
  8035. if (markerHeight != this.lastMarkerHeight) {
  8036. this.lastMarkerHeight = markerHeight;
  8037. util.forEach(this.items, function (item) {
  8038. item.dirty = true;
  8039. if (item.displayed) item.redraw();
  8040. });
  8041. restack = true;
  8042. }
  8043. // reposition visible items vertically
  8044. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  8045. stack.stack(this.visibleItems, margin, restack);
  8046. }
  8047. else { // no stacking
  8048. stack.nostack(this.visibleItems, margin);
  8049. }
  8050. // recalculate the height of the group
  8051. var height;
  8052. var visibleItems = this.visibleItems;
  8053. if (visibleItems.length) {
  8054. var min = visibleItems[0].top;
  8055. var max = visibleItems[0].top + visibleItems[0].height;
  8056. util.forEach(visibleItems, function (item) {
  8057. min = Math.min(min, item.top);
  8058. max = Math.max(max, (item.top + item.height));
  8059. });
  8060. height = (max - min) + margin.axis + margin.item;
  8061. }
  8062. else {
  8063. height = margin.axis + margin.item;
  8064. }
  8065. height = Math.max(height, this.props.label.height);
  8066. // calculate actual size and position
  8067. var foreground = this.dom.foreground;
  8068. this.top = foreground.offsetTop;
  8069. this.left = foreground.offsetLeft;
  8070. this.width = foreground.offsetWidth;
  8071. resized = util.updateProperty(this, 'height', height) || resized;
  8072. // recalculate size of label
  8073. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  8074. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  8075. // apply new height
  8076. this.dom.background.style.height = height + 'px';
  8077. this.dom.foreground.style.height = height + 'px';
  8078. this.dom.label.style.height = height + 'px';
  8079. // update vertical position of items after they are re-stacked and the height of the group is calculated
  8080. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  8081. var item = this.visibleItems[i];
  8082. item.repositionY();
  8083. }
  8084. return resized;
  8085. };
  8086. /**
  8087. * Show this group: attach to the DOM
  8088. */
  8089. Group.prototype.show = function() {
  8090. if (!this.dom.label.parentNode) {
  8091. this.itemSet.dom.labelSet.appendChild(this.dom.label);
  8092. }
  8093. if (!this.dom.foreground.parentNode) {
  8094. this.itemSet.dom.foreground.appendChild(this.dom.foreground);
  8095. }
  8096. if (!this.dom.background.parentNode) {
  8097. this.itemSet.dom.background.appendChild(this.dom.background);
  8098. }
  8099. if (!this.dom.axis.parentNode) {
  8100. this.itemSet.dom.axis.appendChild(this.dom.axis);
  8101. }
  8102. };
  8103. /**
  8104. * Hide this group: remove from the DOM
  8105. */
  8106. Group.prototype.hide = function() {
  8107. var label = this.dom.label;
  8108. if (label.parentNode) {
  8109. label.parentNode.removeChild(label);
  8110. }
  8111. var foreground = this.dom.foreground;
  8112. if (foreground.parentNode) {
  8113. foreground.parentNode.removeChild(foreground);
  8114. }
  8115. var background = this.dom.background;
  8116. if (background.parentNode) {
  8117. background.parentNode.removeChild(background);
  8118. }
  8119. var axis = this.dom.axis;
  8120. if (axis.parentNode) {
  8121. axis.parentNode.removeChild(axis);
  8122. }
  8123. };
  8124. /**
  8125. * Add an item to the group
  8126. * @param {Item} item
  8127. */
  8128. Group.prototype.add = function(item) {
  8129. this.items[item.id] = item;
  8130. item.setParent(this);
  8131. if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
  8132. var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
  8133. this._checkIfVisible(item, this.visibleItems, range);
  8134. }
  8135. };
  8136. /**
  8137. * Remove an item from the group
  8138. * @param {Item} item
  8139. */
  8140. Group.prototype.remove = function(item) {
  8141. delete this.items[item.id];
  8142. item.setParent(this.itemSet);
  8143. // remove from visible items
  8144. var index = this.visibleItems.indexOf(item);
  8145. if (index != -1) this.visibleItems.splice(index, 1);
  8146. // TODO: also remove from ordered items?
  8147. };
  8148. /**
  8149. * Remove an item from the corresponding DataSet
  8150. * @param {Item} item
  8151. */
  8152. Group.prototype.removeFromDataSet = function(item) {
  8153. this.itemSet.removeItem(item.id);
  8154. };
  8155. /**
  8156. * Reorder the items
  8157. */
  8158. Group.prototype.order = function() {
  8159. var array = util.toArray(this.items);
  8160. this.orderedItems.byStart = array;
  8161. this.orderedItems.byEnd = this._constructByEndArray(array);
  8162. stack.orderByStart(this.orderedItems.byStart);
  8163. stack.orderByEnd(this.orderedItems.byEnd);
  8164. };
  8165. /**
  8166. * Create an array containing all items being a range (having an end date)
  8167. * @param {Item[]} array
  8168. * @returns {ItemRange[]}
  8169. * @private
  8170. */
  8171. Group.prototype._constructByEndArray = function(array) {
  8172. var endArray = [];
  8173. for (var i = 0; i < array.length; i++) {
  8174. if (array[i] instanceof ItemRange) {
  8175. endArray.push(array[i]);
  8176. }
  8177. }
  8178. return endArray;
  8179. };
  8180. /**
  8181. * Update the visible items
  8182. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  8183. * @param {Item[]} visibleItems The previously visible items.
  8184. * @param {{start: number, end: number}} range Visible range
  8185. * @return {Item[]} visibleItems The new visible items.
  8186. * @private
  8187. */
  8188. Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
  8189. var initialPosByStart,
  8190. newVisibleItems = [],
  8191. i;
  8192. // first check if the items that were in view previously are still in view.
  8193. // this handles the case for the ItemRange that is both before and after the current one.
  8194. if (visibleItems.length > 0) {
  8195. for (i = 0; i < visibleItems.length; i++) {
  8196. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  8197. }
  8198. }
  8199. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  8200. if (newVisibleItems.length == 0) {
  8201. initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
  8202. }
  8203. else {
  8204. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  8205. }
  8206. // use visible search to find a visible ItemRange (only based on endTime)
  8207. var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
  8208. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  8209. if (initialPosByStart != -1) {
  8210. for (i = initialPosByStart; i >= 0; i--) {
  8211. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  8212. }
  8213. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  8214. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  8215. }
  8216. }
  8217. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  8218. if (initialPosByEnd != -1) {
  8219. for (i = initialPosByEnd; i >= 0; i--) {
  8220. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  8221. }
  8222. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  8223. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  8224. }
  8225. }
  8226. return newVisibleItems;
  8227. };
  8228. /**
  8229. * this function checks if an item is invisible. If it is NOT we make it visible
  8230. * and add it to the global visible items. If it is, return true.
  8231. *
  8232. * @param {Item} item
  8233. * @param {Item[]} visibleItems
  8234. * @param {{start:number, end:number}} range
  8235. * @returns {boolean}
  8236. * @private
  8237. */
  8238. Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
  8239. if (item.isVisible(range)) {
  8240. if (!item.displayed) item.show();
  8241. item.repositionX();
  8242. if (visibleItems.indexOf(item) == -1) {
  8243. visibleItems.push(item);
  8244. }
  8245. return false;
  8246. }
  8247. else {
  8248. return true;
  8249. }
  8250. };
  8251. /**
  8252. * this function is very similar to the _checkIfInvisible() but it does not
  8253. * return booleans, hides the item if it should not be seen and always adds to
  8254. * the visibleItems.
  8255. * this one is for brute forcing and hiding.
  8256. *
  8257. * @param {Item} item
  8258. * @param {Array} visibleItems
  8259. * @param {{start:number, end:number}} range
  8260. * @private
  8261. */
  8262. Group.prototype._checkIfVisible = function(item, visibleItems, range) {
  8263. if (item.isVisible(range)) {
  8264. if (!item.displayed) item.show();
  8265. // reposition item horizontally
  8266. item.repositionX();
  8267. visibleItems.push(item);
  8268. }
  8269. else {
  8270. if (item.displayed) item.hide();
  8271. }
  8272. };
  8273. /**
  8274. * Create a timeline visualization
  8275. * @param {HTMLElement} container
  8276. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  8277. * @param {Object} [options] See Timeline.setOptions for the available options.
  8278. * @constructor
  8279. */
  8280. function Timeline (container, items, options) {
  8281. if (!(this instanceof Timeline)) {
  8282. throw new SyntaxError('Constructor must be called with the new operator');
  8283. }
  8284. var me = this;
  8285. this.defaultOptions = {
  8286. start: null,
  8287. end: null,
  8288. autoResize: true,
  8289. orientation: 'bottom',
  8290. width: null,
  8291. height: null,
  8292. maxHeight: null,
  8293. minHeight: null
  8294. };
  8295. this.options = util.deepExtend({}, this.defaultOptions);
  8296. // Create the DOM, props, and emitter
  8297. this._create(container);
  8298. // all components listed here will be repainted automatically
  8299. this.components = [];
  8300. this.body = {
  8301. dom: this.dom,
  8302. domProps: this.props,
  8303. emitter: {
  8304. on: this.on.bind(this),
  8305. off: this.off.bind(this),
  8306. emit: this.emit.bind(this)
  8307. },
  8308. util: {
  8309. snap: null, // will be specified after TimeAxis is created
  8310. toScreen: me._toScreen.bind(me),
  8311. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  8312. toTime: me._toTime.bind(me),
  8313. toGlobalTime : me._toGlobalTime.bind(me)
  8314. }
  8315. };
  8316. // range
  8317. this.range = new Range(this.body);
  8318. this.components.push(this.range);
  8319. this.body.range = this.range;
  8320. // time axis
  8321. this.timeAxis = new TimeAxis(this.body);
  8322. this.components.push(this.timeAxis);
  8323. this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
  8324. // current time bar
  8325. this.currentTime = new CurrentTime(this.body);
  8326. this.components.push(this.currentTime);
  8327. // custom time bar
  8328. // Note: time bar will be attached in this.setOptions when selected
  8329. this.customTime = new CustomTime(this.body);
  8330. this.components.push(this.customTime);
  8331. // item set
  8332. this.itemSet = new ItemSet(this.body);
  8333. this.components.push(this.itemSet);
  8334. this.itemsData = null; // DataSet
  8335. this.groupsData = null; // DataSet
  8336. // apply options
  8337. if (options) {
  8338. this.setOptions(options);
  8339. }
  8340. // create itemset
  8341. if (items) {
  8342. this.setItems(items);
  8343. }
  8344. else {
  8345. this.redraw();
  8346. }
  8347. }
  8348. // turn Timeline into an event emitter
  8349. Emitter(Timeline.prototype);
  8350. /**
  8351. * Create the main DOM for the Timeline: a root panel containing left, right,
  8352. * top, bottom, content, and background panel.
  8353. * @param {Element} container The container element where the Timeline will
  8354. * be attached.
  8355. * @private
  8356. */
  8357. Timeline.prototype._create = function (container) {
  8358. this.dom = {};
  8359. this.dom.root = document.createElement('div');
  8360. this.dom.background = document.createElement('div');
  8361. this.dom.backgroundVertical = document.createElement('div');
  8362. this.dom.backgroundHorizontal = document.createElement('div');
  8363. this.dom.centerContainer = document.createElement('div');
  8364. this.dom.leftContainer = document.createElement('div');
  8365. this.dom.rightContainer = document.createElement('div');
  8366. this.dom.center = document.createElement('div');
  8367. this.dom.left = document.createElement('div');
  8368. this.dom.right = document.createElement('div');
  8369. this.dom.top = document.createElement('div');
  8370. this.dom.bottom = document.createElement('div');
  8371. this.dom.shadowTop = document.createElement('div');
  8372. this.dom.shadowBottom = document.createElement('div');
  8373. this.dom.shadowTopLeft = document.createElement('div');
  8374. this.dom.shadowBottomLeft = document.createElement('div');
  8375. this.dom.shadowTopRight = document.createElement('div');
  8376. this.dom.shadowBottomRight = document.createElement('div');
  8377. this.dom.background.className = 'vispanel background';
  8378. this.dom.backgroundVertical.className = 'vispanel background vertical';
  8379. this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
  8380. this.dom.centerContainer.className = 'vispanel center';
  8381. this.dom.leftContainer.className = 'vispanel left';
  8382. this.dom.rightContainer.className = 'vispanel right';
  8383. this.dom.top.className = 'vispanel top';
  8384. this.dom.bottom.className = 'vispanel bottom';
  8385. this.dom.left.className = 'content';
  8386. this.dom.center.className = 'content';
  8387. this.dom.right.className = 'content';
  8388. this.dom.shadowTop.className = 'shadow top';
  8389. this.dom.shadowBottom.className = 'shadow bottom';
  8390. this.dom.shadowTopLeft.className = 'shadow top';
  8391. this.dom.shadowBottomLeft.className = 'shadow bottom';
  8392. this.dom.shadowTopRight.className = 'shadow top';
  8393. this.dom.shadowBottomRight.className = 'shadow bottom';
  8394. this.dom.root.appendChild(this.dom.background);
  8395. this.dom.root.appendChild(this.dom.backgroundVertical);
  8396. this.dom.root.appendChild(this.dom.backgroundHorizontal);
  8397. this.dom.root.appendChild(this.dom.centerContainer);
  8398. this.dom.root.appendChild(this.dom.leftContainer);
  8399. this.dom.root.appendChild(this.dom.rightContainer);
  8400. this.dom.root.appendChild(this.dom.top);
  8401. this.dom.root.appendChild(this.dom.bottom);
  8402. this.dom.centerContainer.appendChild(this.dom.center);
  8403. this.dom.leftContainer.appendChild(this.dom.left);
  8404. this.dom.rightContainer.appendChild(this.dom.right);
  8405. this.dom.centerContainer.appendChild(this.dom.shadowTop);
  8406. this.dom.centerContainer.appendChild(this.dom.shadowBottom);
  8407. this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
  8408. this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
  8409. this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
  8410. this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
  8411. this.on('rangechange', this.redraw.bind(this));
  8412. this.on('change', this.redraw.bind(this));
  8413. this.on('touch', this._onTouch.bind(this));
  8414. this.on('pinch', this._onPinch.bind(this));
  8415. this.on('dragstart', this._onDragStart.bind(this));
  8416. this.on('drag', this._onDrag.bind(this));
  8417. // create event listeners for all interesting events, these events will be
  8418. // emitted via emitter
  8419. this.hammer = Hammer(this.dom.root, {
  8420. prevent_default: true
  8421. });
  8422. this.listeners = {};
  8423. var me = this;
  8424. var events = [
  8425. 'touch', 'pinch',
  8426. 'tap', 'doubletap', 'hold',
  8427. 'dragstart', 'drag', 'dragend',
  8428. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
  8429. ];
  8430. events.forEach(function (event) {
  8431. var listener = function () {
  8432. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  8433. me.emit.apply(me, args);
  8434. };
  8435. me.hammer.on(event, listener);
  8436. me.listeners[event] = listener;
  8437. });
  8438. // size properties of each of the panels
  8439. this.props = {
  8440. root: {},
  8441. background: {},
  8442. centerContainer: {},
  8443. leftContainer: {},
  8444. rightContainer: {},
  8445. center: {},
  8446. left: {},
  8447. right: {},
  8448. top: {},
  8449. bottom: {},
  8450. border: {},
  8451. scrollTop: 0,
  8452. scrollTopMin: 0
  8453. };
  8454. this.touch = {}; // store state information needed for touch events
  8455. // attach the root panel to the provided container
  8456. if (!container) throw new Error('No container provided');
  8457. container.appendChild(this.dom.root);
  8458. };
  8459. /**
  8460. * Destroy the Timeline, clean up all DOM elements and event listeners.
  8461. */
  8462. Timeline.prototype.destroy = function () {
  8463. // unbind datasets
  8464. this.clear();
  8465. // remove all event listeners
  8466. this.off();
  8467. // stop checking for changed size
  8468. this._stopAutoResize();
  8469. // remove from DOM
  8470. if (this.dom.root.parentNode) {
  8471. this.dom.root.parentNode.removeChild(this.dom.root);
  8472. }
  8473. this.dom = null;
  8474. // cleanup hammer touch events
  8475. for (var event in this.listeners) {
  8476. if (this.listeners.hasOwnProperty(event)) {
  8477. delete this.listeners[event];
  8478. }
  8479. }
  8480. this.listeners = null;
  8481. this.hammer = null;
  8482. // give all components the opportunity to cleanup
  8483. this.components.forEach(function (component) {
  8484. component.destroy();
  8485. });
  8486. this.body = null;
  8487. };
  8488. /**
  8489. * Set options. Options will be passed to all components loaded in the Timeline.
  8490. * @param {Object} [options]
  8491. * {String} orientation
  8492. * Vertical orientation for the Timeline,
  8493. * can be 'bottom' (default) or 'top'.
  8494. * {String | Number} width
  8495. * Width for the timeline, a number in pixels or
  8496. * a css string like '1000px' or '75%'. '100%' by default.
  8497. * {String | Number} height
  8498. * Fixed height for the Timeline, a number in pixels or
  8499. * a css string like '400px' or '75%'. If undefined,
  8500. * The Timeline will automatically size such that
  8501. * its contents fit.
  8502. * {String | Number} minHeight
  8503. * Minimum height for the Timeline, a number in pixels or
  8504. * a css string like '400px' or '75%'.
  8505. * {String | Number} maxHeight
  8506. * Maximum height for the Timeline, a number in pixels or
  8507. * a css string like '400px' or '75%'.
  8508. * {Number | Date | String} start
  8509. * Start date for the visible window
  8510. * {Number | Date | String} end
  8511. * End date for the visible window
  8512. */
  8513. Timeline.prototype.setOptions = function (options) {
  8514. if (options) {
  8515. // copy the known options
  8516. var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
  8517. util.selectiveExtend(fields, this.options, options);
  8518. // enable/disable autoResize
  8519. this._initAutoResize();
  8520. }
  8521. // propagate options to all components
  8522. this.components.forEach(function (component) {
  8523. component.setOptions(options);
  8524. });
  8525. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  8526. if (options && options.order) {
  8527. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  8528. }
  8529. // redraw everything
  8530. this.redraw();
  8531. };
  8532. /**
  8533. * Set a custom time bar
  8534. * @param {Date} time
  8535. */
  8536. Timeline.prototype.setCustomTime = function (time) {
  8537. if (!this.customTime) {
  8538. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  8539. }
  8540. this.customTime.setCustomTime(time);
  8541. };
  8542. /**
  8543. * Retrieve the current custom time.
  8544. * @return {Date} customTime
  8545. */
  8546. Timeline.prototype.getCustomTime = function() {
  8547. if (!this.customTime) {
  8548. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  8549. }
  8550. return this.customTime.getCustomTime();
  8551. };
  8552. /**
  8553. * Set items
  8554. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  8555. */
  8556. Timeline.prototype.setItems = function(items) {
  8557. var initialLoad = (this.itemsData == null);
  8558. // convert to type DataSet when needed
  8559. var newDataSet;
  8560. if (!items) {
  8561. newDataSet = null;
  8562. }
  8563. else if (items instanceof DataSet || items instanceof DataView) {
  8564. newDataSet = items;
  8565. }
  8566. else {
  8567. // turn an array into a dataset
  8568. newDataSet = new DataSet(items, {
  8569. type: {
  8570. start: 'Date',
  8571. end: 'Date'
  8572. }
  8573. });
  8574. }
  8575. // set items
  8576. this.itemsData = newDataSet;
  8577. this.itemSet && this.itemSet.setItems(newDataSet);
  8578. if (initialLoad && ('start' in this.options || 'end' in this.options)) {
  8579. this.fit();
  8580. var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
  8581. var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
  8582. this.setWindow(start, end);
  8583. }
  8584. };
  8585. /**
  8586. * Set groups
  8587. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  8588. */
  8589. Timeline.prototype.setGroups = function(groups) {
  8590. // convert to type DataSet when needed
  8591. var newDataSet;
  8592. if (!groups) {
  8593. newDataSet = null;
  8594. }
  8595. else if (groups instanceof DataSet || groups instanceof DataView) {
  8596. newDataSet = groups;
  8597. }
  8598. else {
  8599. // turn an array into a dataset
  8600. newDataSet = new DataSet(groups);
  8601. }
  8602. this.groupsData = newDataSet;
  8603. this.itemSet.setGroups(newDataSet);
  8604. };
  8605. /**
  8606. * Clear the Timeline. By Default, items, groups and options are cleared.
  8607. * Example usage:
  8608. *
  8609. * timeline.clear(); // clear items, groups, and options
  8610. * timeline.clear({options: true}); // clear options only
  8611. *
  8612. * @param {Object} [what] Optionally specify what to clear. By default:
  8613. * {items: true, groups: true, options: true}
  8614. */
  8615. Timeline.prototype.clear = function(what) {
  8616. // clear items
  8617. if (!what || what.items) {
  8618. this.setItems(null);
  8619. }
  8620. // clear groups
  8621. if (!what || what.groups) {
  8622. this.setGroups(null);
  8623. }
  8624. // clear options of timeline and of each of the components
  8625. if (!what || what.options) {
  8626. this.components.forEach(function (component) {
  8627. component.setOptions(component.defaultOptions);
  8628. });
  8629. this.setOptions(this.defaultOptions); // this will also do a redraw
  8630. }
  8631. };
  8632. /**
  8633. * Set Timeline window such that it fits all items
  8634. */
  8635. Timeline.prototype.fit = function() {
  8636. // apply the data range as range
  8637. var dataRange = this.getItemRange();
  8638. // add 5% space on both sides
  8639. var start = dataRange.min;
  8640. var end = dataRange.max;
  8641. if (start != null && end != null) {
  8642. var interval = (end.valueOf() - start.valueOf());
  8643. if (interval <= 0) {
  8644. // prevent an empty interval
  8645. interval = 24 * 60 * 60 * 1000; // 1 day
  8646. }
  8647. start = new Date(start.valueOf() - interval * 0.05);
  8648. end = new Date(end.valueOf() + interval * 0.05);
  8649. }
  8650. // skip range set if there is no start and end date
  8651. if (start === null && end === null) {
  8652. return;
  8653. }
  8654. this.range.setRange(start, end);
  8655. };
  8656. /**
  8657. * Get the data range of the item set.
  8658. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  8659. * When no minimum is found, min==null
  8660. * When no maximum is found, max==null
  8661. */
  8662. Timeline.prototype.getItemRange = function() {
  8663. // calculate min from start filed
  8664. var itemsData = this.itemsData,
  8665. min = null,
  8666. max = null;
  8667. if (itemsData) {
  8668. // calculate the minimum value of the field 'start'
  8669. var minItem = itemsData.min('start');
  8670. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  8671. // Note: we convert first to Date and then to number because else
  8672. // a conversion from ISODate to Number will fail
  8673. // calculate maximum value of fields 'start' and 'end'
  8674. var maxStartItem = itemsData.max('start');
  8675. if (maxStartItem) {
  8676. max = util.convert(maxStartItem.start, 'Date').valueOf();
  8677. }
  8678. var maxEndItem = itemsData.max('end');
  8679. if (maxEndItem) {
  8680. if (max == null) {
  8681. max = util.convert(maxEndItem.end, 'Date').valueOf();
  8682. }
  8683. else {
  8684. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  8685. }
  8686. }
  8687. }
  8688. return {
  8689. min: (min != null) ? new Date(min) : null,
  8690. max: (max != null) ? new Date(max) : null
  8691. };
  8692. };
  8693. /**
  8694. * Set selected items by their id. Replaces the current selection
  8695. * Unknown id's are silently ignored.
  8696. * @param {Array} [ids] An array with zero or more id's of the items to be
  8697. * selected. If ids is an empty array, all items will be
  8698. * unselected.
  8699. */
  8700. Timeline.prototype.setSelection = function(ids) {
  8701. this.itemSet && this.itemSet.setSelection(ids);
  8702. };
  8703. /**
  8704. * Get the selected items by their id
  8705. * @return {Array} ids The ids of the selected items
  8706. */
  8707. Timeline.prototype.getSelection = function() {
  8708. return this.itemSet && this.itemSet.getSelection() || [];
  8709. };
  8710. /**
  8711. * Set the visible window. Both parameters are optional, you can change only
  8712. * start or only end. Syntax:
  8713. *
  8714. * TimeLine.setWindow(start, end)
  8715. * TimeLine.setWindow(range)
  8716. *
  8717. * Where start and end can be a Date, number, or string, and range is an
  8718. * object with properties start and end.
  8719. *
  8720. * @param {Date | Number | String | Object} [start] Start date of visible window
  8721. * @param {Date | Number | String} [end] End date of visible window
  8722. */
  8723. Timeline.prototype.setWindow = function(start, end) {
  8724. if (arguments.length == 1) {
  8725. var range = arguments[0];
  8726. this.range.setRange(range.start, range.end);
  8727. }
  8728. else {
  8729. this.range.setRange(start, end);
  8730. }
  8731. };
  8732. /**
  8733. * Get the visible window
  8734. * @return {{start: Date, end: Date}} Visible range
  8735. */
  8736. Timeline.prototype.getWindow = function() {
  8737. var range = this.range.getRange();
  8738. return {
  8739. start: new Date(range.start),
  8740. end: new Date(range.end)
  8741. };
  8742. };
  8743. /**
  8744. * Force a redraw of the Timeline. Can be useful to manually redraw when
  8745. * option autoResize=false
  8746. */
  8747. Timeline.prototype.redraw = function() {
  8748. var resized = false,
  8749. options = this.options,
  8750. props = this.props,
  8751. dom = this.dom;
  8752. if (!dom) return; // when destroyed
  8753. // update class names
  8754. dom.root.className = 'vis timeline root ' + options.orientation;
  8755. // update root width and height options
  8756. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  8757. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  8758. dom.root.style.width = util.option.asSize(options.width, '');
  8759. // calculate border widths
  8760. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  8761. props.border.right = props.border.left;
  8762. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  8763. props.border.bottom = props.border.top;
  8764. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  8765. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  8766. // calculate the heights. If any of the side panels is empty, we set the height to
  8767. // minus the border width, such that the border will be invisible
  8768. props.center.height = dom.center.offsetHeight;
  8769. props.left.height = dom.left.offsetHeight;
  8770. props.right.height = dom.right.offsetHeight;
  8771. props.top.height = dom.top.clientHeight || -props.border.top;
  8772. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  8773. // TODO: compensate borders when any of the panels is empty.
  8774. // apply auto height
  8775. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  8776. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  8777. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  8778. borderRootHeight + props.border.top + props.border.bottom;
  8779. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  8780. // calculate heights of the content panels
  8781. props.root.height = dom.root.offsetHeight;
  8782. props.background.height = props.root.height - borderRootHeight;
  8783. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  8784. borderRootHeight;
  8785. props.centerContainer.height = containerHeight;
  8786. props.leftContainer.height = containerHeight;
  8787. props.rightContainer.height = props.leftContainer.height;
  8788. // calculate the widths of the panels
  8789. props.root.width = dom.root.offsetWidth;
  8790. props.background.width = props.root.width - borderRootWidth;
  8791. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  8792. props.leftContainer.width = props.left.width;
  8793. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  8794. props.rightContainer.width = props.right.width;
  8795. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  8796. props.center.width = centerWidth;
  8797. props.centerContainer.width = centerWidth;
  8798. props.top.width = centerWidth;
  8799. props.bottom.width = centerWidth;
  8800. // resize the panels
  8801. dom.background.style.height = props.background.height + 'px';
  8802. dom.backgroundVertical.style.height = props.background.height + 'px';
  8803. dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
  8804. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  8805. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  8806. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  8807. dom.background.style.width = props.background.width + 'px';
  8808. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  8809. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  8810. dom.centerContainer.style.width = props.center.width + 'px';
  8811. dom.top.style.width = props.top.width + 'px';
  8812. dom.bottom.style.width = props.bottom.width + 'px';
  8813. // reposition the panels
  8814. dom.background.style.left = '0';
  8815. dom.background.style.top = '0';
  8816. dom.backgroundVertical.style.left = props.left.width + 'px';
  8817. dom.backgroundVertical.style.top = '0';
  8818. dom.backgroundHorizontal.style.left = '0';
  8819. dom.backgroundHorizontal.style.top = props.top.height + 'px';
  8820. dom.centerContainer.style.left = props.left.width + 'px';
  8821. dom.centerContainer.style.top = props.top.height + 'px';
  8822. dom.leftContainer.style.left = '0';
  8823. dom.leftContainer.style.top = props.top.height + 'px';
  8824. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  8825. dom.rightContainer.style.top = props.top.height + 'px';
  8826. dom.top.style.left = props.left.width + 'px';
  8827. dom.top.style.top = '0';
  8828. dom.bottom.style.left = props.left.width + 'px';
  8829. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  8830. // update the scrollTop, feasible range for the offset can be changed
  8831. // when the height of the Timeline or of the contents of the center changed
  8832. this._updateScrollTop();
  8833. // reposition the scrollable contents
  8834. var offset = this.props.scrollTop;
  8835. // if (options.orientation == 'bottom') {
  8836. // offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0);
  8837. // }
  8838. dom.center.style.left = '0';
  8839. dom.center.style.top = offset + 'px';
  8840. dom.left.style.left = '0';
  8841. dom.left.style.top = offset + 'px';
  8842. dom.right.style.left = '0';
  8843. dom.right.style.top = offset + 'px';
  8844. // show shadows when vertical scrolling is available
  8845. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  8846. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  8847. dom.shadowTop.style.visibility = visibilityTop;
  8848. dom.shadowBottom.style.visibility = visibilityBottom;
  8849. dom.shadowTopLeft.style.visibility = visibilityTop;
  8850. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  8851. dom.shadowTopRight.style.visibility = visibilityTop;
  8852. dom.shadowBottomRight.style.visibility = visibilityBottom;
  8853. // redraw all components
  8854. this.components.forEach(function (component) {
  8855. resized = component.redraw() || resized;
  8856. });
  8857. if (resized) {
  8858. // keep repainting until all sizes are settled
  8859. this.redraw();
  8860. }
  8861. };
  8862. // TODO: deprecated since version 1.1.0, remove some day
  8863. Timeline.prototype.repaint = function () {
  8864. throw new Error('Function repaint is deprecated. Use redraw instead.');
  8865. };
  8866. /**
  8867. * Convert a position on screen (pixels) to a datetime
  8868. * @param {int} x Position on the screen in pixels
  8869. * @return {Date} time The datetime the corresponds with given position x
  8870. * @private
  8871. */
  8872. // TODO: move this function to Range
  8873. Timeline.prototype._toTime = function(x) {
  8874. var conversion = this.range.conversion(this.props.center.width);
  8875. return new Date(x / conversion.scale + conversion.offset);
  8876. };
  8877. /**
  8878. * Convert a position on the global screen (pixels) to a datetime
  8879. * @param {int} x Position on the screen in pixels
  8880. * @return {Date} time The datetime the corresponds with given position x
  8881. * @private
  8882. */
  8883. // TODO: move this function to Range
  8884. Timeline.prototype._toGlobalTime = function(x) {
  8885. var conversion = this.range.conversion(this.props.root.width);
  8886. return new Date(x / conversion.scale + conversion.offset);
  8887. };
  8888. /**
  8889. * Convert a datetime (Date object) into a position on the screen
  8890. * @param {Date} time A date
  8891. * @return {int} x The position on the screen in pixels which corresponds
  8892. * with the given date.
  8893. * @private
  8894. */
  8895. // TODO: move this function to Range
  8896. Timeline.prototype._toScreen = function(time) {
  8897. var conversion = this.range.conversion(this.props.center.width);
  8898. return (time.valueOf() - conversion.offset) * conversion.scale;
  8899. };
  8900. /**
  8901. * Convert a datetime (Date object) into a position on the root
  8902. * This is used to get the pixel density estimate for the screen, not the center panel
  8903. * @param {Date} time A date
  8904. * @return {int} x The position on root in pixels which corresponds
  8905. * with the given date.
  8906. * @private
  8907. */
  8908. // TODO: move this function to Range
  8909. Timeline.prototype._toGlobalScreen = function(time) {
  8910. var conversion = this.range.conversion(this.props.root.width);
  8911. return (time.valueOf() - conversion.offset) * conversion.scale;
  8912. };
  8913. /**
  8914. * Initialize watching when option autoResize is true
  8915. * @private
  8916. */
  8917. Timeline.prototype._initAutoResize = function () {
  8918. if (this.options.autoResize == true) {
  8919. this._startAutoResize();
  8920. }
  8921. else {
  8922. this._stopAutoResize();
  8923. }
  8924. };
  8925. /**
  8926. * Watch for changes in the size of the container. On resize, the Panel will
  8927. * automatically redraw itself.
  8928. * @private
  8929. */
  8930. Timeline.prototype._startAutoResize = function () {
  8931. var me = this;
  8932. this._stopAutoResize();
  8933. this._onResize = function() {
  8934. if (me.options.autoResize != true) {
  8935. // stop watching when the option autoResize is changed to false
  8936. me._stopAutoResize();
  8937. return;
  8938. }
  8939. if (me.dom.root) {
  8940. // check whether the frame is resized
  8941. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  8942. (me.dom.root.clientHeight != me.props.lastHeight)) {
  8943. me.props.lastWidth = me.dom.root.clientWidth;
  8944. me.props.lastHeight = me.dom.root.clientHeight;
  8945. me.emit('change');
  8946. }
  8947. }
  8948. };
  8949. // add event listener to window resize
  8950. util.addEventListener(window, 'resize', this._onResize);
  8951. this.watchTimer = setInterval(this._onResize, 1000);
  8952. };
  8953. /**
  8954. * Stop watching for a resize of the frame.
  8955. * @private
  8956. */
  8957. Timeline.prototype._stopAutoResize = function () {
  8958. if (this.watchTimer) {
  8959. clearInterval(this.watchTimer);
  8960. this.watchTimer = undefined;
  8961. }
  8962. // remove event listener on window.resize
  8963. util.removeEventListener(window, 'resize', this._onResize);
  8964. this._onResize = null;
  8965. };
  8966. /**
  8967. * Start moving the timeline vertically
  8968. * @param {Event} event
  8969. * @private
  8970. */
  8971. Timeline.prototype._onTouch = function (event) {
  8972. this.touch.allowDragging = true;
  8973. };
  8974. /**
  8975. * Start moving the timeline vertically
  8976. * @param {Event} event
  8977. * @private
  8978. */
  8979. Timeline.prototype._onPinch = function (event) {
  8980. this.touch.allowDragging = false;
  8981. };
  8982. /**
  8983. * Start moving the timeline vertically
  8984. * @param {Event} event
  8985. * @private
  8986. */
  8987. Timeline.prototype._onDragStart = function (event) {
  8988. this.touch.initialScrollTop = this.props.scrollTop;
  8989. };
  8990. /**
  8991. * Move the timeline vertically
  8992. * @param {Event} event
  8993. * @private
  8994. */
  8995. Timeline.prototype._onDrag = function (event) {
  8996. // refuse to drag when we where pinching to prevent the timeline make a jump
  8997. // when releasing the fingers in opposite order from the touch screen
  8998. if (!this.touch.allowDragging) return;
  8999. var delta = event.gesture.deltaY;
  9000. var oldScrollTop = this._getScrollTop();
  9001. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  9002. if (newScrollTop != oldScrollTop) {
  9003. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  9004. }
  9005. };
  9006. /**
  9007. * Apply a scrollTop
  9008. * @param {Number} scrollTop
  9009. * @returns {Number} scrollTop Returns the applied scrollTop
  9010. * @private
  9011. */
  9012. Timeline.prototype._setScrollTop = function (scrollTop) {
  9013. this.props.scrollTop = scrollTop;
  9014. this._updateScrollTop();
  9015. return this.props.scrollTop;
  9016. };
  9017. /**
  9018. * Update the current scrollTop when the height of the containers has been changed
  9019. * @returns {Number} scrollTop Returns the applied scrollTop
  9020. * @private
  9021. */
  9022. Timeline.prototype._updateScrollTop = function () {
  9023. // recalculate the scrollTopMin
  9024. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  9025. if (scrollTopMin != this.props.scrollTopMin) {
  9026. // in case of bottom orientation, change the scrollTop such that the contents
  9027. // do not move relative to the time axis at the bottom
  9028. if (this.options.orientation == 'bottom') {
  9029. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  9030. }
  9031. this.props.scrollTopMin = scrollTopMin;
  9032. }
  9033. // limit the scrollTop to the feasible scroll range
  9034. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  9035. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  9036. return this.props.scrollTop;
  9037. };
  9038. /**
  9039. * Get the current scrollTop
  9040. * @returns {number} scrollTop
  9041. * @private
  9042. */
  9043. Timeline.prototype._getScrollTop = function () {
  9044. return this.props.scrollTop;
  9045. };
  9046. /**
  9047. * Create a timeline visualization
  9048. * @param {HTMLElement} container
  9049. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  9050. * @param {Object} [options] See Graph2d.setOptions for the available options.
  9051. * @constructor
  9052. */
  9053. function Graph2d (container, items, options, groups) {
  9054. var me = this;
  9055. this.defaultOptions = {
  9056. start: null,
  9057. end: null,
  9058. autoResize: true,
  9059. orientation: 'bottom',
  9060. width: null,
  9061. height: null,
  9062. maxHeight: null,
  9063. minHeight: null
  9064. };
  9065. this.options = util.deepExtend({}, this.defaultOptions);
  9066. // Create the DOM, props, and emitter
  9067. this._create(container);
  9068. // all components listed here will be repainted automatically
  9069. this.components = [];
  9070. this.body = {
  9071. dom: this.dom,
  9072. domProps: this.props,
  9073. emitter: {
  9074. on: this.on.bind(this),
  9075. off: this.off.bind(this),
  9076. emit: this.emit.bind(this)
  9077. },
  9078. util: {
  9079. snap: null, // will be specified after TimeAxis is created
  9080. toScreen: me._toScreen.bind(me),
  9081. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  9082. toTime: me._toTime.bind(me),
  9083. toGlobalTime : me._toGlobalTime.bind(me)
  9084. }
  9085. };
  9086. // range
  9087. this.range = new Range(this.body);
  9088. this.components.push(this.range);
  9089. this.body.range = this.range;
  9090. // time axis
  9091. this.timeAxis = new TimeAxis(this.body);
  9092. this.components.push(this.timeAxis);
  9093. this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
  9094. // current time bar
  9095. this.currentTime = new CurrentTime(this.body);
  9096. this.components.push(this.currentTime);
  9097. // custom time bar
  9098. // Note: time bar will be attached in this.setOptions when selected
  9099. this.customTime = new CustomTime(this.body);
  9100. this.components.push(this.customTime);
  9101. // item set
  9102. this.linegraph = new LineGraph(this.body);
  9103. this.components.push(this.linegraph);
  9104. this.itemsData = null; // DataSet
  9105. this.groupsData = null; // DataSet
  9106. // apply options
  9107. if (options) {
  9108. this.setOptions(options);
  9109. }
  9110. // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
  9111. if (groups) {
  9112. this.setGroups(groups);
  9113. }
  9114. // create itemset
  9115. if (items) {
  9116. this.setItems(items);
  9117. }
  9118. else {
  9119. this.redraw();
  9120. }
  9121. }
  9122. // turn Graph2d into an event emitter
  9123. Emitter(Graph2d.prototype);
  9124. /**
  9125. * Create the main DOM for the Graph2d: a root panel containing left, right,
  9126. * top, bottom, content, and background panel.
  9127. * @param {Element} container The container element where the Graph2d will
  9128. * be attached.
  9129. * @private
  9130. */
  9131. Graph2d.prototype._create = function (container) {
  9132. this.dom = {};
  9133. this.dom.root = document.createElement('div');
  9134. this.dom.background = document.createElement('div');
  9135. this.dom.backgroundVertical = document.createElement('div');
  9136. this.dom.backgroundHorizontalContainer = document.createElement('div');
  9137. this.dom.centerContainer = document.createElement('div');
  9138. this.dom.leftContainer = document.createElement('div');
  9139. this.dom.rightContainer = document.createElement('div');
  9140. this.dom.backgroundHorizontal = document.createElement('div');
  9141. this.dom.center = document.createElement('div');
  9142. this.dom.left = document.createElement('div');
  9143. this.dom.right = document.createElement('div');
  9144. this.dom.top = document.createElement('div');
  9145. this.dom.bottom = document.createElement('div');
  9146. this.dom.shadowTop = document.createElement('div');
  9147. this.dom.shadowBottom = document.createElement('div');
  9148. this.dom.shadowTopLeft = document.createElement('div');
  9149. this.dom.shadowBottomLeft = document.createElement('div');
  9150. this.dom.shadowTopRight = document.createElement('div');
  9151. this.dom.shadowBottomRight = document.createElement('div');
  9152. this.dom.background.className = 'vispanel background';
  9153. this.dom.backgroundVertical.className = 'vispanel background vertical';
  9154. this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal';
  9155. this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
  9156. this.dom.centerContainer.className = 'vispanel center';
  9157. this.dom.leftContainer.className = 'vispanel left';
  9158. this.dom.rightContainer.className = 'vispanel right';
  9159. this.dom.top.className = 'vispanel top';
  9160. this.dom.bottom.className = 'vispanel bottom';
  9161. this.dom.left.className = 'content';
  9162. this.dom.center.className = 'content';
  9163. this.dom.right.className = 'content';
  9164. this.dom.shadowTop.className = 'shadow top';
  9165. this.dom.shadowBottom.className = 'shadow bottom';
  9166. this.dom.shadowTopLeft.className = 'shadow top';
  9167. this.dom.shadowBottomLeft.className = 'shadow bottom';
  9168. this.dom.shadowTopRight.className = 'shadow top';
  9169. this.dom.shadowBottomRight.className = 'shadow bottom';
  9170. this.dom.root.appendChild(this.dom.background);
  9171. this.dom.root.appendChild(this.dom.backgroundVertical);
  9172. this.dom.root.appendChild(this.dom.backgroundHorizontalContainer);
  9173. this.dom.root.appendChild(this.dom.centerContainer);
  9174. this.dom.root.appendChild(this.dom.leftContainer);
  9175. this.dom.root.appendChild(this.dom.rightContainer);
  9176. this.dom.root.appendChild(this.dom.top);
  9177. this.dom.root.appendChild(this.dom.bottom);
  9178. this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal);
  9179. this.dom.centerContainer.appendChild(this.dom.center);
  9180. this.dom.leftContainer.appendChild(this.dom.left);
  9181. this.dom.rightContainer.appendChild(this.dom.right);
  9182. this.dom.centerContainer.appendChild(this.dom.shadowTop);
  9183. this.dom.centerContainer.appendChild(this.dom.shadowBottom);
  9184. this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
  9185. this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
  9186. this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
  9187. this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
  9188. this.on('rangechange', this.redraw.bind(this));
  9189. this.on('change', this.redraw.bind(this));
  9190. this.on('touch', this._onTouch.bind(this));
  9191. this.on('pinch', this._onPinch.bind(this));
  9192. this.on('dragstart', this._onDragStart.bind(this));
  9193. this.on('drag', this._onDrag.bind(this));
  9194. // create event listeners for all interesting events, these events will be
  9195. // emitted via emitter
  9196. this.hammer = Hammer(this.dom.root, {
  9197. prevent_default: true
  9198. });
  9199. this.listeners = {};
  9200. var me = this;
  9201. var events = [
  9202. 'touch', 'pinch',
  9203. 'tap', 'doubletap', 'hold',
  9204. 'dragstart', 'drag', 'dragend',
  9205. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
  9206. ];
  9207. events.forEach(function (event) {
  9208. var listener = function () {
  9209. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  9210. me.emit.apply(me, args);
  9211. };
  9212. me.hammer.on(event, listener);
  9213. me.listeners[event] = listener;
  9214. });
  9215. // size properties of each of the panels
  9216. this.props = {
  9217. root: {},
  9218. background: {},
  9219. centerContainer: {},
  9220. leftContainer: {},
  9221. rightContainer: {},
  9222. center: {},
  9223. left: {},
  9224. right: {},
  9225. top: {},
  9226. bottom: {},
  9227. border: {},
  9228. scrollTop: 0,
  9229. scrollTopMin: 0
  9230. };
  9231. this.touch = {}; // store state information needed for touch events
  9232. // attach the root panel to the provided container
  9233. if (!container) throw new Error('No container provided');
  9234. container.appendChild(this.dom.root);
  9235. };
  9236. /**
  9237. * Destroy the Graph2d, clean up all DOM elements and event listeners.
  9238. */
  9239. Graph2d.prototype.destroy = function () {
  9240. // unbind datasets
  9241. this.clear();
  9242. // remove all event listeners
  9243. this.off();
  9244. // stop checking for changed size
  9245. this._stopAutoResize();
  9246. // remove from DOM
  9247. if (this.dom.root.parentNode) {
  9248. this.dom.root.parentNode.removeChild(this.dom.root);
  9249. }
  9250. this.dom = null;
  9251. // cleanup hammer touch events
  9252. for (var event in this.listeners) {
  9253. if (this.listeners.hasOwnProperty(event)) {
  9254. delete this.listeners[event];
  9255. }
  9256. }
  9257. this.listeners = null;
  9258. this.hammer = null;
  9259. // give all components the opportunity to cleanup
  9260. this.components.forEach(function (component) {
  9261. component.destroy();
  9262. });
  9263. this.body = null;
  9264. };
  9265. /**
  9266. * Set options. Options will be passed to all components loaded in the Graph2d.
  9267. * @param {Object} [options]
  9268. * {String} orientation
  9269. * Vertical orientation for the Graph2d,
  9270. * can be 'bottom' (default) or 'top'.
  9271. * {String | Number} width
  9272. * Width for the timeline, a number in pixels or
  9273. * a css string like '1000px' or '75%'. '100%' by default.
  9274. * {String | Number} height
  9275. * Fixed height for the Graph2d, a number in pixels or
  9276. * a css string like '400px' or '75%'. If undefined,
  9277. * The Graph2d will automatically size such that
  9278. * its contents fit.
  9279. * {String | Number} minHeight
  9280. * Minimum height for the Graph2d, a number in pixels or
  9281. * a css string like '400px' or '75%'.
  9282. * {String | Number} maxHeight
  9283. * Maximum height for the Graph2d, a number in pixels or
  9284. * a css string like '400px' or '75%'.
  9285. * {Number | Date | String} start
  9286. * Start date for the visible window
  9287. * {Number | Date | String} end
  9288. * End date for the visible window
  9289. */
  9290. Graph2d.prototype.setOptions = function (options) {
  9291. if (options) {
  9292. // copy the known options
  9293. var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
  9294. util.selectiveExtend(fields, this.options, options);
  9295. // enable/disable autoResize
  9296. this._initAutoResize();
  9297. }
  9298. // propagate options to all components
  9299. this.components.forEach(function (component) {
  9300. component.setOptions(options);
  9301. });
  9302. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  9303. if (options && options.order) {
  9304. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  9305. }
  9306. // redraw everything
  9307. this.redraw();
  9308. };
  9309. /**
  9310. * Set a custom time bar
  9311. * @param {Date} time
  9312. */
  9313. Graph2d.prototype.setCustomTime = function (time) {
  9314. if (!this.customTime) {
  9315. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  9316. }
  9317. this.customTime.setCustomTime(time);
  9318. };
  9319. /**
  9320. * Retrieve the current custom time.
  9321. * @return {Date} customTime
  9322. */
  9323. Graph2d.prototype.getCustomTime = function() {
  9324. if (!this.customTime) {
  9325. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  9326. }
  9327. return this.customTime.getCustomTime();
  9328. };
  9329. /**
  9330. * Set items
  9331. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  9332. */
  9333. Graph2d.prototype.setItems = function(items) {
  9334. var initialLoad = (this.itemsData == null);
  9335. // convert to type DataSet when needed
  9336. var newDataSet;
  9337. if (!items) {
  9338. newDataSet = null;
  9339. }
  9340. else if (items instanceof DataSet || items instanceof DataView) {
  9341. newDataSet = items;
  9342. }
  9343. else {
  9344. // turn an array into a dataset
  9345. newDataSet = new DataSet(items, {
  9346. type: {
  9347. start: 'Date',
  9348. end: 'Date'
  9349. }
  9350. });
  9351. }
  9352. // set items
  9353. this.itemsData = newDataSet;
  9354. this.linegraph && this.linegraph.setItems(newDataSet);
  9355. if (initialLoad && ('start' in this.options || 'end' in this.options)) {
  9356. this.fit();
  9357. var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
  9358. var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
  9359. this.setWindow(start, end);
  9360. }
  9361. };
  9362. /**
  9363. * Set groups
  9364. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  9365. */
  9366. Graph2d.prototype.setGroups = function(groups) {
  9367. // convert to type DataSet when needed
  9368. var newDataSet;
  9369. if (!groups) {
  9370. newDataSet = null;
  9371. }
  9372. else if (groups instanceof DataSet || groups instanceof DataView) {
  9373. newDataSet = groups;
  9374. }
  9375. else {
  9376. // turn an array into a dataset
  9377. newDataSet = new DataSet(groups);
  9378. }
  9379. this.groupsData = newDataSet;
  9380. this.linegraph.setGroups(newDataSet);
  9381. };
  9382. /**
  9383. * Clear the Graph2d. By Default, items, groups and options are cleared.
  9384. * Example usage:
  9385. *
  9386. * timeline.clear(); // clear items, groups, and options
  9387. * timeline.clear({options: true}); // clear options only
  9388. *
  9389. * @param {Object} [what] Optionally specify what to clear. By default:
  9390. * {items: true, groups: true, options: true}
  9391. */
  9392. Graph2d.prototype.clear = function(what) {
  9393. // clear items
  9394. if (!what || what.items) {
  9395. this.setItems(null);
  9396. }
  9397. // clear groups
  9398. if (!what || what.groups) {
  9399. this.setGroups(null);
  9400. }
  9401. // clear options of timeline and of each of the components
  9402. if (!what || what.options) {
  9403. this.components.forEach(function (component) {
  9404. component.setOptions(component.defaultOptions);
  9405. });
  9406. this.setOptions(this.defaultOptions); // this will also do a redraw
  9407. }
  9408. };
  9409. /**
  9410. * Set Graph2d window such that it fits all items
  9411. */
  9412. Graph2d.prototype.fit = function() {
  9413. // apply the data range as range
  9414. var dataRange = this.getItemRange();
  9415. // add 5% space on both sides
  9416. var start = dataRange.min;
  9417. var end = dataRange.max;
  9418. if (start != null && end != null) {
  9419. var interval = (end.valueOf() - start.valueOf());
  9420. if (interval <= 0) {
  9421. // prevent an empty interval
  9422. interval = 24 * 60 * 60 * 1000; // 1 day
  9423. }
  9424. start = new Date(start.valueOf() - interval * 0.05);
  9425. end = new Date(end.valueOf() + interval * 0.05);
  9426. }
  9427. // skip range set if there is no start and end date
  9428. if (start === null && end === null) {
  9429. return;
  9430. }
  9431. this.range.setRange(start, end);
  9432. };
  9433. /**
  9434. * Get the data range of the item set.
  9435. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  9436. * When no minimum is found, min==null
  9437. * When no maximum is found, max==null
  9438. */
  9439. Graph2d.prototype.getItemRange = function() {
  9440. // calculate min from start filed
  9441. var itemsData = this.itemsData,
  9442. min = null,
  9443. max = null;
  9444. if (itemsData) {
  9445. // calculate the minimum value of the field 'start'
  9446. var minItem = itemsData.min('start');
  9447. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  9448. // Note: we convert first to Date and then to number because else
  9449. // a conversion from ISODate to Number will fail
  9450. // calculate maximum value of fields 'start' and 'end'
  9451. var maxStartItem = itemsData.max('start');
  9452. if (maxStartItem) {
  9453. max = util.convert(maxStartItem.start, 'Date').valueOf();
  9454. }
  9455. var maxEndItem = itemsData.max('end');
  9456. if (maxEndItem) {
  9457. if (max == null) {
  9458. max = util.convert(maxEndItem.end, 'Date').valueOf();
  9459. }
  9460. else {
  9461. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  9462. }
  9463. }
  9464. }
  9465. return {
  9466. min: (min != null) ? new Date(min) : null,
  9467. max: (max != null) ? new Date(max) : null
  9468. };
  9469. };
  9470. /**
  9471. * Set the visible window. Both parameters are optional, you can change only
  9472. * start or only end. Syntax:
  9473. *
  9474. * TimeLine.setWindow(start, end)
  9475. * TimeLine.setWindow(range)
  9476. *
  9477. * Where start and end can be a Date, number, or string, and range is an
  9478. * object with properties start and end.
  9479. *
  9480. * @param {Date | Number | String | Object} [start] Start date of visible window
  9481. * @param {Date | Number | String} [end] End date of visible window
  9482. */
  9483. Graph2d.prototype.setWindow = function(start, end) {
  9484. if (arguments.length == 1) {
  9485. var range = arguments[0];
  9486. this.range.setRange(range.start, range.end);
  9487. }
  9488. else {
  9489. this.range.setRange(start, end);
  9490. }
  9491. };
  9492. /**
  9493. * Get the visible window
  9494. * @return {{start: Date, end: Date}} Visible range
  9495. */
  9496. Graph2d.prototype.getWindow = function() {
  9497. var range = this.range.getRange();
  9498. return {
  9499. start: new Date(range.start),
  9500. end: new Date(range.end)
  9501. };
  9502. };
  9503. /**
  9504. * Force a redraw of the Graph2d. Can be useful to manually redraw when
  9505. * option autoResize=false
  9506. */
  9507. Graph2d.prototype.redraw = function() {
  9508. var resized = false,
  9509. options = this.options,
  9510. props = this.props,
  9511. dom = this.dom;
  9512. if (!dom) return; // when destroyed
  9513. // update class names
  9514. dom.root.className = 'vis timeline root ' + options.orientation;
  9515. // update root width and height options
  9516. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  9517. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  9518. dom.root.style.width = util.option.asSize(options.width, '');
  9519. // calculate border widths
  9520. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  9521. props.border.right = props.border.left;
  9522. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  9523. props.border.bottom = props.border.top;
  9524. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  9525. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  9526. // calculate the heights. If any of the side panels is empty, we set the height to
  9527. // minus the border width, such that the border will be invisible
  9528. props.center.height = dom.center.offsetHeight;
  9529. props.left.height = dom.left.offsetHeight;
  9530. props.right.height = dom.right.offsetHeight;
  9531. props.top.height = dom.top.clientHeight || -props.border.top;
  9532. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  9533. // TODO: compensate borders when any of the panels is empty.
  9534. // apply auto height
  9535. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  9536. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  9537. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  9538. borderRootHeight + props.border.top + props.border.bottom;
  9539. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  9540. // calculate heights of the content panels
  9541. props.root.height = dom.root.offsetHeight;
  9542. props.background.height = props.root.height - borderRootHeight;
  9543. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  9544. borderRootHeight;
  9545. props.centerContainer.height = containerHeight;
  9546. props.leftContainer.height = containerHeight;
  9547. props.rightContainer.height = props.leftContainer.height;
  9548. // calculate the widths of the panels
  9549. props.root.width = dom.root.offsetWidth;
  9550. props.background.width = props.root.width - borderRootWidth;
  9551. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  9552. props.leftContainer.width = props.left.width;
  9553. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  9554. props.rightContainer.width = props.right.width;
  9555. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  9556. props.center.width = centerWidth;
  9557. props.centerContainer.width = centerWidth;
  9558. props.top.width = centerWidth;
  9559. props.bottom.width = centerWidth;
  9560. // resize the panels
  9561. dom.background.style.height = props.background.height + 'px';
  9562. dom.backgroundVertical.style.height = props.background.height + 'px';
  9563. dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px';
  9564. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  9565. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  9566. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  9567. dom.background.style.width = props.background.width + 'px';
  9568. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  9569. dom.backgroundHorizontalContainer.style.width = props.background.width + 'px';
  9570. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  9571. dom.centerContainer.style.width = props.center.width + 'px';
  9572. dom.top.style.width = props.top.width + 'px';
  9573. dom.bottom.style.width = props.bottom.width + 'px';
  9574. // reposition the panels
  9575. dom.background.style.left = '0';
  9576. dom.background.style.top = '0';
  9577. dom.backgroundVertical.style.left = props.left.width + 'px';
  9578. dom.backgroundVertical.style.top = '0';
  9579. dom.backgroundHorizontalContainer.style.left = '0';
  9580. dom.backgroundHorizontalContainer.style.top = props.top.height + 'px';
  9581. dom.centerContainer.style.left = props.left.width + 'px';
  9582. dom.centerContainer.style.top = props.top.height + 'px';
  9583. dom.leftContainer.style.left = '0';
  9584. dom.leftContainer.style.top = props.top.height + 'px';
  9585. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  9586. dom.rightContainer.style.top = props.top.height + 'px';
  9587. dom.top.style.left = props.left.width + 'px';
  9588. dom.top.style.top = '0';
  9589. dom.bottom.style.left = props.left.width + 'px';
  9590. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  9591. // update the scrollTop, feasible range for the offset can be changed
  9592. // when the height of the Graph2d or of the contents of the center changed
  9593. this._updateScrollTop();
  9594. // reposition the scrollable contents
  9595. var offset = this.props.scrollTop;
  9596. // if (options.orientation == 'bottom') {
  9597. // offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0);
  9598. // }
  9599. dom.center.style.left = '0';
  9600. dom.center.style.top = offset + 'px';
  9601. dom.backgroundHorizontal.style.left = '0';
  9602. dom.backgroundHorizontal.style.top = offset + 'px';
  9603. dom.left.style.left = '0';
  9604. dom.left.style.top = offset + 'px';
  9605. dom.right.style.left = '0';
  9606. dom.right.style.top = offset + 'px';
  9607. // show shadows when vertical scrolling is available
  9608. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  9609. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  9610. dom.shadowTop.style.visibility = visibilityTop;
  9611. dom.shadowBottom.style.visibility = visibilityBottom;
  9612. dom.shadowTopLeft.style.visibility = visibilityTop;
  9613. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  9614. dom.shadowTopRight.style.visibility = visibilityTop;
  9615. dom.shadowBottomRight.style.visibility = visibilityBottom;
  9616. // redraw all components
  9617. this.components.forEach(function (component) {
  9618. resized = component.redraw() || resized;
  9619. });
  9620. if (resized) {
  9621. // keep redrawing until all sizes are settled
  9622. this.redraw();
  9623. }
  9624. };
  9625. /**
  9626. * Convert a position on screen (pixels) to a datetime
  9627. * @param {int} x Position on the screen in pixels
  9628. * @return {Date} time The datetime the corresponds with given position x
  9629. * @private
  9630. */
  9631. // TODO: move this function to Range
  9632. Graph2d.prototype._toTime = function(x) {
  9633. var conversion = this.range.conversion(this.props.center.width);
  9634. return new Date(x / conversion.scale + conversion.offset);
  9635. };
  9636. /**
  9637. * Convert a datetime (Date object) into a position on the root
  9638. * This is used to get the pixel density estimate for the screen, not the center panel
  9639. * @param {Date} time A date
  9640. * @return {int} x The position on root in pixels which corresponds
  9641. * with the given date.
  9642. * @private
  9643. */
  9644. // TODO: move this function to Range
  9645. Graph2d.prototype._toGlobalTime = function(x) {
  9646. var conversion = this.range.conversion(this.props.root.width);
  9647. return new Date(x / conversion.scale + conversion.offset);
  9648. };
  9649. /**
  9650. * Convert a datetime (Date object) into a position on the screen
  9651. * @param {Date} time A date
  9652. * @return {int} x The position on the screen in pixels which corresponds
  9653. * with the given date.
  9654. * @private
  9655. */
  9656. // TODO: move this function to Range
  9657. Graph2d.prototype._toScreen = function(time) {
  9658. var conversion = this.range.conversion(this.props.center.width);
  9659. return (time.valueOf() - conversion.offset) * conversion.scale;
  9660. };
  9661. /**
  9662. * Convert a datetime (Date object) into a position on the root
  9663. * This is used to get the pixel density estimate for the screen, not the center panel
  9664. * @param {Date} time A date
  9665. * @return {int} x The position on root in pixels which corresponds
  9666. * with the given date.
  9667. * @private
  9668. */
  9669. // TODO: move this function to Range
  9670. Graph2d.prototype._toGlobalScreen = function(time) {
  9671. var conversion = this.range.conversion(this.props.root.width);
  9672. return (time.valueOf() - conversion.offset) * conversion.scale;
  9673. };
  9674. /**
  9675. * Initialize watching when option autoResize is true
  9676. * @private
  9677. */
  9678. Graph2d.prototype._initAutoResize = function () {
  9679. if (this.options.autoResize == true) {
  9680. this._startAutoResize();
  9681. }
  9682. else {
  9683. this._stopAutoResize();
  9684. }
  9685. };
  9686. /**
  9687. * Watch for changes in the size of the container. On resize, the Panel will
  9688. * automatically redraw itself.
  9689. * @private
  9690. */
  9691. Graph2d.prototype._startAutoResize = function () {
  9692. var me = this;
  9693. this._stopAutoResize();
  9694. this._onResize = function() {
  9695. if (me.options.autoResize != true) {
  9696. // stop watching when the option autoResize is changed to false
  9697. me._stopAutoResize();
  9698. return;
  9699. }
  9700. if (me.dom.root) {
  9701. // check whether the frame is resized
  9702. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  9703. (me.dom.root.clientHeight != me.props.lastHeight)) {
  9704. me.props.lastWidth = me.dom.root.clientWidth;
  9705. me.props.lastHeight = me.dom.root.clientHeight;
  9706. me.emit('change');
  9707. }
  9708. }
  9709. };
  9710. // add event listener to window resize
  9711. util.addEventListener(window, 'resize', this._onResize);
  9712. this.watchTimer = setInterval(this._onResize, 1000);
  9713. };
  9714. /**
  9715. * Stop watching for a resize of the frame.
  9716. * @private
  9717. */
  9718. Graph2d.prototype._stopAutoResize = function () {
  9719. if (this.watchTimer) {
  9720. clearInterval(this.watchTimer);
  9721. this.watchTimer = undefined;
  9722. }
  9723. // remove event listener on window.resize
  9724. util.removeEventListener(window, 'resize', this._onResize);
  9725. this._onResize = null;
  9726. };
  9727. /**
  9728. * Start moving the timeline vertically
  9729. * @param {Event} event
  9730. * @private
  9731. */
  9732. Graph2d.prototype._onTouch = function (event) {
  9733. this.touch.allowDragging = true;
  9734. };
  9735. /**
  9736. * Start moving the timeline vertically
  9737. * @param {Event} event
  9738. * @private
  9739. */
  9740. Graph2d.prototype._onPinch = function (event) {
  9741. this.touch.allowDragging = false;
  9742. };
  9743. /**
  9744. * Start moving the timeline vertically
  9745. * @param {Event} event
  9746. * @private
  9747. */
  9748. Graph2d.prototype._onDragStart = function (event) {
  9749. this.touch.initialScrollTop = this.props.scrollTop;
  9750. };
  9751. /**
  9752. * Move the timeline vertically
  9753. * @param {Event} event
  9754. * @private
  9755. */
  9756. Graph2d.prototype._onDrag = function (event) {
  9757. // refuse to drag when we where pinching to prevent the timeline make a jump
  9758. // when releasing the fingers in opposite order from the touch screen
  9759. if (!this.touch.allowDragging) return;
  9760. var delta = event.gesture.deltaY;
  9761. var oldScrollTop = this._getScrollTop();
  9762. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  9763. if (newScrollTop != oldScrollTop) {
  9764. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  9765. }
  9766. };
  9767. /**
  9768. * Apply a scrollTop
  9769. * @param {Number} scrollTop
  9770. * @returns {Number} scrollTop Returns the applied scrollTop
  9771. * @private
  9772. */
  9773. Graph2d.prototype._setScrollTop = function (scrollTop) {
  9774. this.props.scrollTop = scrollTop;
  9775. this._updateScrollTop();
  9776. return this.props.scrollTop;
  9777. };
  9778. /**
  9779. * Update the current scrollTop when the height of the containers has been changed
  9780. * @returns {Number} scrollTop Returns the applied scrollTop
  9781. * @private
  9782. */
  9783. Graph2d.prototype._updateScrollTop = function () {
  9784. // recalculate the scrollTopMin
  9785. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  9786. if (scrollTopMin != this.props.scrollTopMin) {
  9787. // in case of bottom orientation, change the scrollTop such that the contents
  9788. // do not move relative to the time axis at the bottom
  9789. if (this.options.orientation == 'bottom') {
  9790. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  9791. }
  9792. this.props.scrollTopMin = scrollTopMin;
  9793. }
  9794. // limit the scrollTop to the feasible scroll range
  9795. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  9796. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  9797. return this.props.scrollTop;
  9798. };
  9799. /**
  9800. * Get the current scrollTop
  9801. * @returns {number} scrollTop
  9802. * @private
  9803. */
  9804. Graph2d.prototype._getScrollTop = function () {
  9805. return this.props.scrollTop;
  9806. };
  9807. (function(exports) {
  9808. /**
  9809. * Parse a text source containing data in DOT language into a JSON object.
  9810. * The object contains two lists: one with nodes and one with edges.
  9811. *
  9812. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  9813. *
  9814. * @param {String} data Text containing a graph in DOT-notation
  9815. * @return {Object} graph An object containing two parameters:
  9816. * {Object[]} nodes
  9817. * {Object[]} edges
  9818. */
  9819. function parseDOT (data) {
  9820. dot = data;
  9821. return parseGraph();
  9822. }
  9823. // token types enumeration
  9824. var TOKENTYPE = {
  9825. NULL : 0,
  9826. DELIMITER : 1,
  9827. IDENTIFIER: 2,
  9828. UNKNOWN : 3
  9829. };
  9830. // map with all delimiters
  9831. var DELIMITERS = {
  9832. '{': true,
  9833. '}': true,
  9834. '[': true,
  9835. ']': true,
  9836. ';': true,
  9837. '=': true,
  9838. ',': true,
  9839. '->': true,
  9840. '--': true
  9841. };
  9842. var dot = ''; // current dot file
  9843. var index = 0; // current index in dot file
  9844. var c = ''; // current token character in expr
  9845. var token = ''; // current token
  9846. var tokenType = TOKENTYPE.NULL; // type of the token
  9847. /**
  9848. * Get the first character from the dot file.
  9849. * The character is stored into the char c. If the end of the dot file is
  9850. * reached, the function puts an empty string in c.
  9851. */
  9852. function first() {
  9853. index = 0;
  9854. c = dot.charAt(0);
  9855. }
  9856. /**
  9857. * Get the next character from the dot file.
  9858. * The character is stored into the char c. If the end of the dot file is
  9859. * reached, the function puts an empty string in c.
  9860. */
  9861. function next() {
  9862. index++;
  9863. c = dot.charAt(index);
  9864. }
  9865. /**
  9866. * Preview the next character from the dot file.
  9867. * @return {String} cNext
  9868. */
  9869. function nextPreview() {
  9870. return dot.charAt(index + 1);
  9871. }
  9872. /**
  9873. * Test whether given character is alphabetic or numeric
  9874. * @param {String} c
  9875. * @return {Boolean} isAlphaNumeric
  9876. */
  9877. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  9878. function isAlphaNumeric(c) {
  9879. return regexAlphaNumeric.test(c);
  9880. }
  9881. /**
  9882. * Merge all properties of object b into object b
  9883. * @param {Object} a
  9884. * @param {Object} b
  9885. * @return {Object} a
  9886. */
  9887. function merge (a, b) {
  9888. if (!a) {
  9889. a = {};
  9890. }
  9891. if (b) {
  9892. for (var name in b) {
  9893. if (b.hasOwnProperty(name)) {
  9894. a[name] = b[name];
  9895. }
  9896. }
  9897. }
  9898. return a;
  9899. }
  9900. /**
  9901. * Set a value in an object, where the provided parameter name can be a
  9902. * path with nested parameters. For example:
  9903. *
  9904. * var obj = {a: 2};
  9905. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  9906. *
  9907. * @param {Object} obj
  9908. * @param {String} path A parameter name or dot-separated parameter path,
  9909. * like "color.highlight.border".
  9910. * @param {*} value
  9911. */
  9912. function setValue(obj, path, value) {
  9913. var keys = path.split('.');
  9914. var o = obj;
  9915. while (keys.length) {
  9916. var key = keys.shift();
  9917. if (keys.length) {
  9918. // this isn't the end point
  9919. if (!o[key]) {
  9920. o[key] = {};
  9921. }
  9922. o = o[key];
  9923. }
  9924. else {
  9925. // this is the end point
  9926. o[key] = value;
  9927. }
  9928. }
  9929. }
  9930. /**
  9931. * Add a node to a graph object. If there is already a node with
  9932. * the same id, their attributes will be merged.
  9933. * @param {Object} graph
  9934. * @param {Object} node
  9935. */
  9936. function addNode(graph, node) {
  9937. var i, len;
  9938. var current = null;
  9939. // find root graph (in case of subgraph)
  9940. var graphs = [graph]; // list with all graphs from current graph to root graph
  9941. var root = graph;
  9942. while (root.parent) {
  9943. graphs.push(root.parent);
  9944. root = root.parent;
  9945. }
  9946. // find existing node (at root level) by its id
  9947. if (root.nodes) {
  9948. for (i = 0, len = root.nodes.length; i < len; i++) {
  9949. if (node.id === root.nodes[i].id) {
  9950. current = root.nodes[i];
  9951. break;
  9952. }
  9953. }
  9954. }
  9955. if (!current) {
  9956. // this is a new node
  9957. current = {
  9958. id: node.id
  9959. };
  9960. if (graph.node) {
  9961. // clone default attributes
  9962. current.attr = merge(current.attr, graph.node);
  9963. }
  9964. }
  9965. // add node to this (sub)graph and all its parent graphs
  9966. for (i = graphs.length - 1; i >= 0; i--) {
  9967. var g = graphs[i];
  9968. if (!g.nodes) {
  9969. g.nodes = [];
  9970. }
  9971. if (g.nodes.indexOf(current) == -1) {
  9972. g.nodes.push(current);
  9973. }
  9974. }
  9975. // merge attributes
  9976. if (node.attr) {
  9977. current.attr = merge(current.attr, node.attr);
  9978. }
  9979. }
  9980. /**
  9981. * Add an edge to a graph object
  9982. * @param {Object} graph
  9983. * @param {Object} edge
  9984. */
  9985. function addEdge(graph, edge) {
  9986. if (!graph.edges) {
  9987. graph.edges = [];
  9988. }
  9989. graph.edges.push(edge);
  9990. if (graph.edge) {
  9991. var attr = merge({}, graph.edge); // clone default attributes
  9992. edge.attr = merge(attr, edge.attr); // merge attributes
  9993. }
  9994. }
  9995. /**
  9996. * Create an edge to a graph object
  9997. * @param {Object} graph
  9998. * @param {String | Number | Object} from
  9999. * @param {String | Number | Object} to
  10000. * @param {String} type
  10001. * @param {Object | null} attr
  10002. * @return {Object} edge
  10003. */
  10004. function createEdge(graph, from, to, type, attr) {
  10005. var edge = {
  10006. from: from,
  10007. to: to,
  10008. type: type
  10009. };
  10010. if (graph.edge) {
  10011. edge.attr = merge({}, graph.edge); // clone default attributes
  10012. }
  10013. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  10014. return edge;
  10015. }
  10016. /**
  10017. * Get next token in the current dot file.
  10018. * The token and token type are available as token and tokenType
  10019. */
  10020. function getToken() {
  10021. tokenType = TOKENTYPE.NULL;
  10022. token = '';
  10023. // skip over whitespaces
  10024. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10025. next();
  10026. }
  10027. do {
  10028. var isComment = false;
  10029. // skip comment
  10030. if (c == '#') {
  10031. // find the previous non-space character
  10032. var i = index - 1;
  10033. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  10034. i--;
  10035. }
  10036. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  10037. // the # is at the start of a line, this is indeed a line comment
  10038. while (c != '' && c != '\n') {
  10039. next();
  10040. }
  10041. isComment = true;
  10042. }
  10043. }
  10044. if (c == '/' && nextPreview() == '/') {
  10045. // skip line comment
  10046. while (c != '' && c != '\n') {
  10047. next();
  10048. }
  10049. isComment = true;
  10050. }
  10051. if (c == '/' && nextPreview() == '*') {
  10052. // skip block comment
  10053. while (c != '') {
  10054. if (c == '*' && nextPreview() == '/') {
  10055. // end of block comment found. skip these last two characters
  10056. next();
  10057. next();
  10058. break;
  10059. }
  10060. else {
  10061. next();
  10062. }
  10063. }
  10064. isComment = true;
  10065. }
  10066. // skip over whitespaces
  10067. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10068. next();
  10069. }
  10070. }
  10071. while (isComment);
  10072. // check for end of dot file
  10073. if (c == '') {
  10074. // token is still empty
  10075. tokenType = TOKENTYPE.DELIMITER;
  10076. return;
  10077. }
  10078. // check for delimiters consisting of 2 characters
  10079. var c2 = c + nextPreview();
  10080. if (DELIMITERS[c2]) {
  10081. tokenType = TOKENTYPE.DELIMITER;
  10082. token = c2;
  10083. next();
  10084. next();
  10085. return;
  10086. }
  10087. // check for delimiters consisting of 1 character
  10088. if (DELIMITERS[c]) {
  10089. tokenType = TOKENTYPE.DELIMITER;
  10090. token = c;
  10091. next();
  10092. return;
  10093. }
  10094. // check for an identifier (number or string)
  10095. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  10096. if (isAlphaNumeric(c) || c == '-') {
  10097. token += c;
  10098. next();
  10099. while (isAlphaNumeric(c)) {
  10100. token += c;
  10101. next();
  10102. }
  10103. if (token == 'false') {
  10104. token = false; // convert to boolean
  10105. }
  10106. else if (token == 'true') {
  10107. token = true; // convert to boolean
  10108. }
  10109. else if (!isNaN(Number(token))) {
  10110. token = Number(token); // convert to number
  10111. }
  10112. tokenType = TOKENTYPE.IDENTIFIER;
  10113. return;
  10114. }
  10115. // check for a string enclosed by double quotes
  10116. if (c == '"') {
  10117. next();
  10118. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  10119. token += c;
  10120. if (c == '"') { // skip the escape character
  10121. next();
  10122. }
  10123. next();
  10124. }
  10125. if (c != '"') {
  10126. throw newSyntaxError('End of string " expected');
  10127. }
  10128. next();
  10129. tokenType = TOKENTYPE.IDENTIFIER;
  10130. return;
  10131. }
  10132. // something unknown is found, wrong characters, a syntax error
  10133. tokenType = TOKENTYPE.UNKNOWN;
  10134. while (c != '') {
  10135. token += c;
  10136. next();
  10137. }
  10138. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  10139. }
  10140. /**
  10141. * Parse a graph.
  10142. * @returns {Object} graph
  10143. */
  10144. function parseGraph() {
  10145. var graph = {};
  10146. first();
  10147. getToken();
  10148. // optional strict keyword
  10149. if (token == 'strict') {
  10150. graph.strict = true;
  10151. getToken();
  10152. }
  10153. // graph or digraph keyword
  10154. if (token == 'graph' || token == 'digraph') {
  10155. graph.type = token;
  10156. getToken();
  10157. }
  10158. // optional graph id
  10159. if (tokenType == TOKENTYPE.IDENTIFIER) {
  10160. graph.id = token;
  10161. getToken();
  10162. }
  10163. // open angle bracket
  10164. if (token != '{') {
  10165. throw newSyntaxError('Angle bracket { expected');
  10166. }
  10167. getToken();
  10168. // statements
  10169. parseStatements(graph);
  10170. // close angle bracket
  10171. if (token != '}') {
  10172. throw newSyntaxError('Angle bracket } expected');
  10173. }
  10174. getToken();
  10175. // end of file
  10176. if (token !== '') {
  10177. throw newSyntaxError('End of file expected');
  10178. }
  10179. getToken();
  10180. // remove temporary default properties
  10181. delete graph.node;
  10182. delete graph.edge;
  10183. delete graph.graph;
  10184. return graph;
  10185. }
  10186. /**
  10187. * Parse a list with statements.
  10188. * @param {Object} graph
  10189. */
  10190. function parseStatements (graph) {
  10191. while (token !== '' && token != '}') {
  10192. parseStatement(graph);
  10193. if (token == ';') {
  10194. getToken();
  10195. }
  10196. }
  10197. }
  10198. /**
  10199. * Parse a single statement. Can be a an attribute statement, node
  10200. * statement, a series of node statements and edge statements, or a
  10201. * parameter.
  10202. * @param {Object} graph
  10203. */
  10204. function parseStatement(graph) {
  10205. // parse subgraph
  10206. var subgraph = parseSubgraph(graph);
  10207. if (subgraph) {
  10208. // edge statements
  10209. parseEdge(graph, subgraph);
  10210. return;
  10211. }
  10212. // parse an attribute statement
  10213. var attr = parseAttributeStatement(graph);
  10214. if (attr) {
  10215. return;
  10216. }
  10217. // parse node
  10218. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10219. throw newSyntaxError('Identifier expected');
  10220. }
  10221. var id = token; // id can be a string or a number
  10222. getToken();
  10223. if (token == '=') {
  10224. // id statement
  10225. getToken();
  10226. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10227. throw newSyntaxError('Identifier expected');
  10228. }
  10229. graph[id] = token;
  10230. getToken();
  10231. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  10232. }
  10233. else {
  10234. parseNodeStatement(graph, id);
  10235. }
  10236. }
  10237. /**
  10238. * Parse a subgraph
  10239. * @param {Object} graph parent graph object
  10240. * @return {Object | null} subgraph
  10241. */
  10242. function parseSubgraph (graph) {
  10243. var subgraph = null;
  10244. // optional subgraph keyword
  10245. if (token == 'subgraph') {
  10246. subgraph = {};
  10247. subgraph.type = 'subgraph';
  10248. getToken();
  10249. // optional graph id
  10250. if (tokenType == TOKENTYPE.IDENTIFIER) {
  10251. subgraph.id = token;
  10252. getToken();
  10253. }
  10254. }
  10255. // open angle bracket
  10256. if (token == '{') {
  10257. getToken();
  10258. if (!subgraph) {
  10259. subgraph = {};
  10260. }
  10261. subgraph.parent = graph;
  10262. subgraph.node = graph.node;
  10263. subgraph.edge = graph.edge;
  10264. subgraph.graph = graph.graph;
  10265. // statements
  10266. parseStatements(subgraph);
  10267. // close angle bracket
  10268. if (token != '}') {
  10269. throw newSyntaxError('Angle bracket } expected');
  10270. }
  10271. getToken();
  10272. // remove temporary default properties
  10273. delete subgraph.node;
  10274. delete subgraph.edge;
  10275. delete subgraph.graph;
  10276. delete subgraph.parent;
  10277. // register at the parent graph
  10278. if (!graph.subgraphs) {
  10279. graph.subgraphs = [];
  10280. }
  10281. graph.subgraphs.push(subgraph);
  10282. }
  10283. return subgraph;
  10284. }
  10285. /**
  10286. * parse an attribute statement like "node [shape=circle fontSize=16]".
  10287. * Available keywords are 'node', 'edge', 'graph'.
  10288. * The previous list with default attributes will be replaced
  10289. * @param {Object} graph
  10290. * @returns {String | null} keyword Returns the name of the parsed attribute
  10291. * (node, edge, graph), or null if nothing
  10292. * is parsed.
  10293. */
  10294. function parseAttributeStatement (graph) {
  10295. // attribute statements
  10296. if (token == 'node') {
  10297. getToken();
  10298. // node attributes
  10299. graph.node = parseAttributeList();
  10300. return 'node';
  10301. }
  10302. else if (token == 'edge') {
  10303. getToken();
  10304. // edge attributes
  10305. graph.edge = parseAttributeList();
  10306. return 'edge';
  10307. }
  10308. else if (token == 'graph') {
  10309. getToken();
  10310. // graph attributes
  10311. graph.graph = parseAttributeList();
  10312. return 'graph';
  10313. }
  10314. return null;
  10315. }
  10316. /**
  10317. * parse a node statement
  10318. * @param {Object} graph
  10319. * @param {String | Number} id
  10320. */
  10321. function parseNodeStatement(graph, id) {
  10322. // node statement
  10323. var node = {
  10324. id: id
  10325. };
  10326. var attr = parseAttributeList();
  10327. if (attr) {
  10328. node.attr = attr;
  10329. }
  10330. addNode(graph, node);
  10331. // edge statements
  10332. parseEdge(graph, id);
  10333. }
  10334. /**
  10335. * Parse an edge or a series of edges
  10336. * @param {Object} graph
  10337. * @param {String | Number} from Id of the from node
  10338. */
  10339. function parseEdge(graph, from) {
  10340. while (token == '->' || token == '--') {
  10341. var to;
  10342. var type = token;
  10343. getToken();
  10344. var subgraph = parseSubgraph(graph);
  10345. if (subgraph) {
  10346. to = subgraph;
  10347. }
  10348. else {
  10349. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10350. throw newSyntaxError('Identifier or subgraph expected');
  10351. }
  10352. to = token;
  10353. addNode(graph, {
  10354. id: to
  10355. });
  10356. getToken();
  10357. }
  10358. // parse edge attributes
  10359. var attr = parseAttributeList();
  10360. // create edge
  10361. var edge = createEdge(graph, from, to, type, attr);
  10362. addEdge(graph, edge);
  10363. from = to;
  10364. }
  10365. }
  10366. /**
  10367. * Parse a set with attributes,
  10368. * for example [label="1.000", shape=solid]
  10369. * @return {Object | null} attr
  10370. */
  10371. function parseAttributeList() {
  10372. var attr = null;
  10373. while (token == '[') {
  10374. getToken();
  10375. attr = {};
  10376. while (token !== '' && token != ']') {
  10377. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10378. throw newSyntaxError('Attribute name expected');
  10379. }
  10380. var name = token;
  10381. getToken();
  10382. if (token != '=') {
  10383. throw newSyntaxError('Equal sign = expected');
  10384. }
  10385. getToken();
  10386. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10387. throw newSyntaxError('Attribute value expected');
  10388. }
  10389. var value = token;
  10390. setValue(attr, name, value); // name can be a path
  10391. getToken();
  10392. if (token ==',') {
  10393. getToken();
  10394. }
  10395. }
  10396. if (token != ']') {
  10397. throw newSyntaxError('Bracket ] expected');
  10398. }
  10399. getToken();
  10400. }
  10401. return attr;
  10402. }
  10403. /**
  10404. * Create a syntax error with extra information on current token and index.
  10405. * @param {String} message
  10406. * @returns {SyntaxError} err
  10407. */
  10408. function newSyntaxError(message) {
  10409. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  10410. }
  10411. /**
  10412. * Chop off text after a maximum length
  10413. * @param {String} text
  10414. * @param {Number} maxLength
  10415. * @returns {String}
  10416. */
  10417. function chop (text, maxLength) {
  10418. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  10419. }
  10420. /**
  10421. * Execute a function fn for each pair of elements in two arrays
  10422. * @param {Array | *} array1
  10423. * @param {Array | *} array2
  10424. * @param {function} fn
  10425. */
  10426. function forEach2(array1, array2, fn) {
  10427. if (array1 instanceof Array) {
  10428. array1.forEach(function (elem1) {
  10429. if (array2 instanceof Array) {
  10430. array2.forEach(function (elem2) {
  10431. fn(elem1, elem2);
  10432. });
  10433. }
  10434. else {
  10435. fn(elem1, array2);
  10436. }
  10437. });
  10438. }
  10439. else {
  10440. if (array2 instanceof Array) {
  10441. array2.forEach(function (elem2) {
  10442. fn(array1, elem2);
  10443. });
  10444. }
  10445. else {
  10446. fn(array1, array2);
  10447. }
  10448. }
  10449. }
  10450. /**
  10451. * Convert a string containing a graph in DOT language into a map containing
  10452. * with nodes and edges in the format of graph.
  10453. * @param {String} data Text containing a graph in DOT-notation
  10454. * @return {Object} graphData
  10455. */
  10456. function DOTToGraph (data) {
  10457. // parse the DOT file
  10458. var dotData = parseDOT(data);
  10459. var graphData = {
  10460. nodes: [],
  10461. edges: [],
  10462. options: {}
  10463. };
  10464. // copy the nodes
  10465. if (dotData.nodes) {
  10466. dotData.nodes.forEach(function (dotNode) {
  10467. var graphNode = {
  10468. id: dotNode.id,
  10469. label: String(dotNode.label || dotNode.id)
  10470. };
  10471. merge(graphNode, dotNode.attr);
  10472. if (graphNode.image) {
  10473. graphNode.shape = 'image';
  10474. }
  10475. graphData.nodes.push(graphNode);
  10476. });
  10477. }
  10478. // copy the edges
  10479. if (dotData.edges) {
  10480. /**
  10481. * Convert an edge in DOT format to an edge with VisGraph format
  10482. * @param {Object} dotEdge
  10483. * @returns {Object} graphEdge
  10484. */
  10485. function convertEdge(dotEdge) {
  10486. var graphEdge = {
  10487. from: dotEdge.from,
  10488. to: dotEdge.to
  10489. };
  10490. merge(graphEdge, dotEdge.attr);
  10491. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  10492. return graphEdge;
  10493. }
  10494. dotData.edges.forEach(function (dotEdge) {
  10495. var from, to;
  10496. if (dotEdge.from instanceof Object) {
  10497. from = dotEdge.from.nodes;
  10498. }
  10499. else {
  10500. from = {
  10501. id: dotEdge.from
  10502. }
  10503. }
  10504. if (dotEdge.to instanceof Object) {
  10505. to = dotEdge.to.nodes;
  10506. }
  10507. else {
  10508. to = {
  10509. id: dotEdge.to
  10510. }
  10511. }
  10512. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  10513. dotEdge.from.edges.forEach(function (subEdge) {
  10514. var graphEdge = convertEdge(subEdge);
  10515. graphData.edges.push(graphEdge);
  10516. });
  10517. }
  10518. forEach2(from, to, function (from, to) {
  10519. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  10520. var graphEdge = convertEdge(subEdge);
  10521. graphData.edges.push(graphEdge);
  10522. });
  10523. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  10524. dotEdge.to.edges.forEach(function (subEdge) {
  10525. var graphEdge = convertEdge(subEdge);
  10526. graphData.edges.push(graphEdge);
  10527. });
  10528. }
  10529. });
  10530. }
  10531. // copy the options
  10532. if (dotData.attr) {
  10533. graphData.options = dotData.attr;
  10534. }
  10535. return graphData;
  10536. }
  10537. // exports
  10538. exports.parseDOT = parseDOT;
  10539. exports.DOTToGraph = DOTToGraph;
  10540. })(typeof util !== 'undefined' ? util : exports);
  10541. /**
  10542. * Canvas shapes used by Network
  10543. */
  10544. if (typeof CanvasRenderingContext2D !== 'undefined') {
  10545. /**
  10546. * Draw a circle shape
  10547. */
  10548. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  10549. this.beginPath();
  10550. this.arc(x, y, r, 0, 2*Math.PI, false);
  10551. };
  10552. /**
  10553. * Draw a square shape
  10554. * @param {Number} x horizontal center
  10555. * @param {Number} y vertical center
  10556. * @param {Number} r size, width and height of the square
  10557. */
  10558. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  10559. this.beginPath();
  10560. this.rect(x - r, y - r, r * 2, r * 2);
  10561. };
  10562. /**
  10563. * Draw a triangle shape
  10564. * @param {Number} x horizontal center
  10565. * @param {Number} y vertical center
  10566. * @param {Number} r radius, half the length of the sides of the triangle
  10567. */
  10568. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  10569. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10570. this.beginPath();
  10571. var s = r * 2;
  10572. var s2 = s / 2;
  10573. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10574. var h = Math.sqrt(s * s - s2 * s2); // height
  10575. this.moveTo(x, y - (h - ir));
  10576. this.lineTo(x + s2, y + ir);
  10577. this.lineTo(x - s2, y + ir);
  10578. this.lineTo(x, y - (h - ir));
  10579. this.closePath();
  10580. };
  10581. /**
  10582. * Draw a triangle shape in downward orientation
  10583. * @param {Number} x horizontal center
  10584. * @param {Number} y vertical center
  10585. * @param {Number} r radius
  10586. */
  10587. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  10588. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10589. this.beginPath();
  10590. var s = r * 2;
  10591. var s2 = s / 2;
  10592. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10593. var h = Math.sqrt(s * s - s2 * s2); // height
  10594. this.moveTo(x, y + (h - ir));
  10595. this.lineTo(x + s2, y - ir);
  10596. this.lineTo(x - s2, y - ir);
  10597. this.lineTo(x, y + (h - ir));
  10598. this.closePath();
  10599. };
  10600. /**
  10601. * Draw a star shape, a star with 5 points
  10602. * @param {Number} x horizontal center
  10603. * @param {Number} y vertical center
  10604. * @param {Number} r radius, half the length of the sides of the triangle
  10605. */
  10606. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  10607. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  10608. this.beginPath();
  10609. for (var n = 0; n < 10; n++) {
  10610. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  10611. this.lineTo(
  10612. x + radius * Math.sin(n * 2 * Math.PI / 10),
  10613. y - radius * Math.cos(n * 2 * Math.PI / 10)
  10614. );
  10615. }
  10616. this.closePath();
  10617. };
  10618. /**
  10619. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  10620. */
  10621. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  10622. var r2d = Math.PI/180;
  10623. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  10624. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  10625. this.beginPath();
  10626. this.moveTo(x+r,y);
  10627. this.lineTo(x+w-r,y);
  10628. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  10629. this.lineTo(x+w,y+h-r);
  10630. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  10631. this.lineTo(x+r,y+h);
  10632. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  10633. this.lineTo(x,y+r);
  10634. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  10635. };
  10636. /**
  10637. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10638. */
  10639. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  10640. var kappa = .5522848,
  10641. ox = (w / 2) * kappa, // control point offset horizontal
  10642. oy = (h / 2) * kappa, // control point offset vertical
  10643. xe = x + w, // x-end
  10644. ye = y + h, // y-end
  10645. xm = x + w / 2, // x-middle
  10646. ym = y + h / 2; // y-middle
  10647. this.beginPath();
  10648. this.moveTo(x, ym);
  10649. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10650. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10651. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10652. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10653. };
  10654. /**
  10655. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10656. */
  10657. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  10658. var f = 1/3;
  10659. var wEllipse = w;
  10660. var hEllipse = h * f;
  10661. var kappa = .5522848,
  10662. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  10663. oy = (hEllipse / 2) * kappa, // control point offset vertical
  10664. xe = x + wEllipse, // x-end
  10665. ye = y + hEllipse, // y-end
  10666. xm = x + wEllipse / 2, // x-middle
  10667. ym = y + hEllipse / 2, // y-middle
  10668. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  10669. yeb = y + h; // y-end, bottom ellipse
  10670. this.beginPath();
  10671. this.moveTo(xe, ym);
  10672. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10673. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10674. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10675. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10676. this.lineTo(xe, ymb);
  10677. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  10678. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  10679. this.lineTo(x, ym);
  10680. };
  10681. /**
  10682. * Draw an arrow point (no line)
  10683. */
  10684. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  10685. // tail
  10686. var xt = x - length * Math.cos(angle);
  10687. var yt = y - length * Math.sin(angle);
  10688. // inner tail
  10689. // TODO: allow to customize different shapes
  10690. var xi = x - length * 0.9 * Math.cos(angle);
  10691. var yi = y - length * 0.9 * Math.sin(angle);
  10692. // left
  10693. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  10694. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  10695. // right
  10696. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  10697. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  10698. this.beginPath();
  10699. this.moveTo(x, y);
  10700. this.lineTo(xl, yl);
  10701. this.lineTo(xi, yi);
  10702. this.lineTo(xr, yr);
  10703. this.closePath();
  10704. };
  10705. /**
  10706. * Sets up the dashedLine functionality for drawing
  10707. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  10708. * @author David Jordan
  10709. * @date 2012-08-08
  10710. */
  10711. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  10712. if (!dashArray) dashArray=[10,5];
  10713. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  10714. var dashCount = dashArray.length;
  10715. this.moveTo(x, y);
  10716. var dx = (x2-x), dy = (y2-y);
  10717. var slope = dy/dx;
  10718. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  10719. var dashIndex=0, draw=true;
  10720. while (distRemaining>=0.1){
  10721. var dashLength = dashArray[dashIndex++%dashCount];
  10722. if (dashLength > distRemaining) dashLength = distRemaining;
  10723. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  10724. if (dx<0) xStep = -xStep;
  10725. x += xStep;
  10726. y += slope*xStep;
  10727. this[draw ? 'lineTo' : 'moveTo'](x,y);
  10728. distRemaining -= dashLength;
  10729. draw = !draw;
  10730. }
  10731. };
  10732. // TODO: add diamond shape
  10733. }
  10734. /**
  10735. * @class Node
  10736. * A node. A node can be connected to other nodes via one or multiple edges.
  10737. * @param {object} properties An object containing properties for the node. All
  10738. * properties are optional, except for the id.
  10739. * {number} id Id of the node. Required
  10740. * {string} label Text label for the node
  10741. * {number} x Horizontal position of the node
  10742. * {number} y Vertical position of the node
  10743. * {string} shape Node shape, available:
  10744. * "database", "circle", "ellipse",
  10745. * "box", "image", "text", "dot",
  10746. * "star", "triangle", "triangleDown",
  10747. * "square"
  10748. * {string} image An image url
  10749. * {string} title An title text, can be HTML
  10750. * {anytype} group A group name or number
  10751. * @param {Network.Images} imagelist A list with images. Only needed
  10752. * when the node has an image
  10753. * @param {Network.Groups} grouplist A list with groups. Needed for
  10754. * retrieving group properties
  10755. * @param {Object} constants An object with default values for
  10756. * example for the color
  10757. *
  10758. */
  10759. function Node(properties, imagelist, grouplist, constants) {
  10760. this.selected = false;
  10761. this.hover = false;
  10762. this.edges = []; // all edges connected to this node
  10763. this.dynamicEdges = [];
  10764. this.reroutedEdges = {};
  10765. this.group = constants.nodes.group;
  10766. this.fontSize = Number(constants.nodes.fontSize);
  10767. this.fontFace = constants.nodes.fontFace;
  10768. this.fontColor = constants.nodes.fontColor;
  10769. this.fontDrawThreshold = 3;
  10770. this.color = constants.nodes.color;
  10771. // set defaults for the properties
  10772. this.id = undefined;
  10773. this.shape = constants.nodes.shape;
  10774. this.image = constants.nodes.image;
  10775. this.x = null;
  10776. this.y = null;
  10777. this.xFixed = false;
  10778. this.yFixed = false;
  10779. this.horizontalAlignLeft = true; // these are for the navigation controls
  10780. this.verticalAlignTop = true; // these are for the navigation controls
  10781. this.radius = constants.nodes.radius;
  10782. this.baseRadiusValue = constants.nodes.radius;
  10783. this.radiusFixed = false;
  10784. this.radiusMin = constants.nodes.radiusMin;
  10785. this.radiusMax = constants.nodes.radiusMax;
  10786. this.level = -1;
  10787. this.preassignedLevel = false;
  10788. this.imagelist = imagelist;
  10789. this.grouplist = grouplist;
  10790. // physics properties
  10791. this.fx = 0.0; // external force x
  10792. this.fy = 0.0; // external force y
  10793. this.vx = 0.0; // velocity x
  10794. this.vy = 0.0; // velocity y
  10795. this.minForce = constants.minForce;
  10796. this.damping = constants.physics.damping;
  10797. this.mass = 1; // kg
  10798. this.fixedData = {x:null,y:null};
  10799. this.setProperties(properties, constants);
  10800. // creating the variables for clustering
  10801. this.resetCluster();
  10802. this.dynamicEdgesLength = 0;
  10803. this.clusterSession = 0;
  10804. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  10805. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  10806. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  10807. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  10808. this.growthIndicator = 0;
  10809. // variables to tell the node about the network.
  10810. this.networkScaleInv = 1;
  10811. this.networkScale = 1;
  10812. this.canvasTopLeft = {"x": -300, "y": -300};
  10813. this.canvasBottomRight = {"x": 300, "y": 300};
  10814. this.parentEdgeId = null;
  10815. }
  10816. /**
  10817. * (re)setting the clustering variables and objects
  10818. */
  10819. Node.prototype.resetCluster = function() {
  10820. // clustering variables
  10821. this.formationScale = undefined; // this is used to determine when to open the cluster
  10822. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  10823. this.containedNodes = {};
  10824. this.containedEdges = {};
  10825. this.clusterSessions = [];
  10826. };
  10827. /**
  10828. * Attach a edge to the node
  10829. * @param {Edge} edge
  10830. */
  10831. Node.prototype.attachEdge = function(edge) {
  10832. if (this.edges.indexOf(edge) == -1) {
  10833. this.edges.push(edge);
  10834. }
  10835. if (this.dynamicEdges.indexOf(edge) == -1) {
  10836. this.dynamicEdges.push(edge);
  10837. }
  10838. this.dynamicEdgesLength = this.dynamicEdges.length;
  10839. };
  10840. /**
  10841. * Detach a edge from the node
  10842. * @param {Edge} edge
  10843. */
  10844. Node.prototype.detachEdge = function(edge) {
  10845. var index = this.edges.indexOf(edge);
  10846. if (index != -1) {
  10847. this.edges.splice(index, 1);
  10848. this.dynamicEdges.splice(index, 1);
  10849. }
  10850. this.dynamicEdgesLength = this.dynamicEdges.length;
  10851. };
  10852. /**
  10853. * Set or overwrite properties for the node
  10854. * @param {Object} properties an object with properties
  10855. * @param {Object} constants and object with default, global properties
  10856. */
  10857. Node.prototype.setProperties = function(properties, constants) {
  10858. if (!properties) {
  10859. return;
  10860. }
  10861. this.originalLabel = undefined;
  10862. // basic properties
  10863. if (properties.id !== undefined) {this.id = properties.id;}
  10864. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  10865. if (properties.title !== undefined) {this.title = properties.title;}
  10866. if (properties.group !== undefined) {this.group = properties.group;}
  10867. if (properties.x !== undefined) {this.x = properties.x;}
  10868. if (properties.y !== undefined) {this.y = properties.y;}
  10869. if (properties.value !== undefined) {this.value = properties.value;}
  10870. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  10871. // physics
  10872. if (properties.mass !== undefined) {this.mass = properties.mass;}
  10873. // navigation controls properties
  10874. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  10875. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  10876. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  10877. if (this.id === undefined) {
  10878. throw "Node must have an id";
  10879. }
  10880. // copy group properties
  10881. if (this.group) {
  10882. var groupObj = this.grouplist.get(this.group);
  10883. for (var prop in groupObj) {
  10884. if (groupObj.hasOwnProperty(prop)) {
  10885. this[prop] = groupObj[prop];
  10886. }
  10887. }
  10888. }
  10889. // individual shape properties
  10890. if (properties.shape !== undefined) {this.shape = properties.shape;}
  10891. if (properties.image !== undefined) {this.image = properties.image;}
  10892. if (properties.radius !== undefined) {this.radius = properties.radius;}
  10893. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  10894. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  10895. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  10896. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  10897. if (this.image !== undefined && this.image != "") {
  10898. if (this.imagelist) {
  10899. this.imageObj = this.imagelist.load(this.image);
  10900. }
  10901. else {
  10902. throw "No imagelist provided";
  10903. }
  10904. }
  10905. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  10906. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  10907. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  10908. if (this.shape == 'image') {
  10909. this.radiusMin = constants.nodes.widthMin;
  10910. this.radiusMax = constants.nodes.widthMax;
  10911. }
  10912. // choose draw method depending on the shape
  10913. switch (this.shape) {
  10914. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  10915. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  10916. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  10917. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10918. // TODO: add diamond shape
  10919. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  10920. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  10921. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  10922. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  10923. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  10924. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  10925. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  10926. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10927. }
  10928. // reset the size of the node, this can be changed
  10929. this._reset();
  10930. };
  10931. /**
  10932. * select this node
  10933. */
  10934. Node.prototype.select = function() {
  10935. this.selected = true;
  10936. this._reset();
  10937. };
  10938. /**
  10939. * unselect this node
  10940. */
  10941. Node.prototype.unselect = function() {
  10942. this.selected = false;
  10943. this._reset();
  10944. };
  10945. /**
  10946. * Reset the calculated size of the node, forces it to recalculate its size
  10947. */
  10948. Node.prototype.clearSizeCache = function() {
  10949. this._reset();
  10950. };
  10951. /**
  10952. * Reset the calculated size of the node, forces it to recalculate its size
  10953. * @private
  10954. */
  10955. Node.prototype._reset = function() {
  10956. this.width = undefined;
  10957. this.height = undefined;
  10958. };
  10959. /**
  10960. * get the title of this node.
  10961. * @return {string} title The title of the node, or undefined when no title
  10962. * has been set.
  10963. */
  10964. Node.prototype.getTitle = function() {
  10965. return typeof this.title === "function" ? this.title() : this.title;
  10966. };
  10967. /**
  10968. * Calculate the distance to the border of the Node
  10969. * @param {CanvasRenderingContext2D} ctx
  10970. * @param {Number} angle Angle in radians
  10971. * @returns {number} distance Distance to the border in pixels
  10972. */
  10973. Node.prototype.distanceToBorder = function (ctx, angle) {
  10974. var borderWidth = 1;
  10975. if (!this.width) {
  10976. this.resize(ctx);
  10977. }
  10978. switch (this.shape) {
  10979. case 'circle':
  10980. case 'dot':
  10981. return this.radius + borderWidth;
  10982. case 'ellipse':
  10983. var a = this.width / 2;
  10984. var b = this.height / 2;
  10985. var w = (Math.sin(angle) * a);
  10986. var h = (Math.cos(angle) * b);
  10987. return a * b / Math.sqrt(w * w + h * h);
  10988. // TODO: implement distanceToBorder for database
  10989. // TODO: implement distanceToBorder for triangle
  10990. // TODO: implement distanceToBorder for triangleDown
  10991. case 'box':
  10992. case 'image':
  10993. case 'text':
  10994. default:
  10995. if (this.width) {
  10996. return Math.min(
  10997. Math.abs(this.width / 2 / Math.cos(angle)),
  10998. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  10999. // TODO: reckon with border radius too in case of box
  11000. }
  11001. else {
  11002. return 0;
  11003. }
  11004. }
  11005. // TODO: implement calculation of distance to border for all shapes
  11006. };
  11007. /**
  11008. * Set forces acting on the node
  11009. * @param {number} fx Force in horizontal direction
  11010. * @param {number} fy Force in vertical direction
  11011. */
  11012. Node.prototype._setForce = function(fx, fy) {
  11013. this.fx = fx;
  11014. this.fy = fy;
  11015. };
  11016. /**
  11017. * Add forces acting on the node
  11018. * @param {number} fx Force in horizontal direction
  11019. * @param {number} fy Force in vertical direction
  11020. * @private
  11021. */
  11022. Node.prototype._addForce = function(fx, fy) {
  11023. this.fx += fx;
  11024. this.fy += fy;
  11025. };
  11026. /**
  11027. * Perform one discrete step for the node
  11028. * @param {number} interval Time interval in seconds
  11029. */
  11030. Node.prototype.discreteStep = function(interval) {
  11031. if (!this.xFixed) {
  11032. var dx = this.damping * this.vx; // damping force
  11033. var ax = (this.fx - dx) / this.mass; // acceleration
  11034. this.vx += ax * interval; // velocity
  11035. this.x += this.vx * interval; // position
  11036. }
  11037. if (!this.yFixed) {
  11038. var dy = this.damping * this.vy; // damping force
  11039. var ay = (this.fy - dy) / this.mass; // acceleration
  11040. this.vy += ay * interval; // velocity
  11041. this.y += this.vy * interval; // position
  11042. }
  11043. };
  11044. /**
  11045. * Perform one discrete step for the node
  11046. * @param {number} interval Time interval in seconds
  11047. * @param {number} maxVelocity The speed limit imposed on the velocity
  11048. */
  11049. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  11050. if (!this.xFixed) {
  11051. var dx = this.damping * this.vx; // damping force
  11052. var ax = (this.fx - dx) / this.mass; // acceleration
  11053. this.vx += ax * interval; // velocity
  11054. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  11055. this.x += this.vx * interval; // position
  11056. }
  11057. else {
  11058. this.fx = 0;
  11059. }
  11060. if (!this.yFixed) {
  11061. var dy = this.damping * this.vy; // damping force
  11062. var ay = (this.fy - dy) / this.mass; // acceleration
  11063. this.vy += ay * interval; // velocity
  11064. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  11065. this.y += this.vy * interval; // position
  11066. }
  11067. else {
  11068. this.fy = 0;
  11069. }
  11070. };
  11071. /**
  11072. * Check if this node has a fixed x and y position
  11073. * @return {boolean} true if fixed, false if not
  11074. */
  11075. Node.prototype.isFixed = function() {
  11076. return (this.xFixed && this.yFixed);
  11077. };
  11078. /**
  11079. * Check if this node is moving
  11080. * @param {number} vmin the minimum velocity considered as "moving"
  11081. * @return {boolean} true if moving, false if it has no velocity
  11082. */
  11083. // TODO: replace this method with calculating the kinetic energy
  11084. Node.prototype.isMoving = function(vmin) {
  11085. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  11086. };
  11087. /**
  11088. * check if this node is selecte
  11089. * @return {boolean} selected True if node is selected, else false
  11090. */
  11091. Node.prototype.isSelected = function() {
  11092. return this.selected;
  11093. };
  11094. /**
  11095. * Retrieve the value of the node. Can be undefined
  11096. * @return {Number} value
  11097. */
  11098. Node.prototype.getValue = function() {
  11099. return this.value;
  11100. };
  11101. /**
  11102. * Calculate the distance from the nodes location to the given location (x,y)
  11103. * @param {Number} x
  11104. * @param {Number} y
  11105. * @return {Number} value
  11106. */
  11107. Node.prototype.getDistance = function(x, y) {
  11108. var dx = this.x - x,
  11109. dy = this.y - y;
  11110. return Math.sqrt(dx * dx + dy * dy);
  11111. };
  11112. /**
  11113. * Adjust the value range of the node. The node will adjust it's radius
  11114. * based on its value.
  11115. * @param {Number} min
  11116. * @param {Number} max
  11117. */
  11118. Node.prototype.setValueRange = function(min, max) {
  11119. if (!this.radiusFixed && this.value !== undefined) {
  11120. if (max == min) {
  11121. this.radius = (this.radiusMin + this.radiusMax) / 2;
  11122. }
  11123. else {
  11124. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  11125. this.radius = (this.value - min) * scale + this.radiusMin;
  11126. }
  11127. }
  11128. this.baseRadiusValue = this.radius;
  11129. };
  11130. /**
  11131. * Draw this node in the given canvas
  11132. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11133. * @param {CanvasRenderingContext2D} ctx
  11134. */
  11135. Node.prototype.draw = function(ctx) {
  11136. throw "Draw method not initialized for node";
  11137. };
  11138. /**
  11139. * Recalculate the size of this node in the given canvas
  11140. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11141. * @param {CanvasRenderingContext2D} ctx
  11142. */
  11143. Node.prototype.resize = function(ctx) {
  11144. throw "Resize method not initialized for node";
  11145. };
  11146. /**
  11147. * Check if this object is overlapping with the provided object
  11148. * @param {Object} obj an object with parameters left, top, right, bottom
  11149. * @return {boolean} True if location is located on node
  11150. */
  11151. Node.prototype.isOverlappingWith = function(obj) {
  11152. return (this.left < obj.right &&
  11153. this.left + this.width > obj.left &&
  11154. this.top < obj.bottom &&
  11155. this.top + this.height > obj.top);
  11156. };
  11157. Node.prototype._resizeImage = function (ctx) {
  11158. // TODO: pre calculate the image size
  11159. if (!this.width || !this.height) { // undefined or 0
  11160. var width, height;
  11161. if (this.value) {
  11162. this.radius = this.baseRadiusValue;
  11163. var scale = this.imageObj.height / this.imageObj.width;
  11164. if (scale !== undefined) {
  11165. width = this.radius || this.imageObj.width;
  11166. height = this.radius * scale || this.imageObj.height;
  11167. }
  11168. else {
  11169. width = 0;
  11170. height = 0;
  11171. }
  11172. }
  11173. else {
  11174. width = this.imageObj.width;
  11175. height = this.imageObj.height;
  11176. }
  11177. this.width = width;
  11178. this.height = height;
  11179. this.growthIndicator = 0;
  11180. if (this.width > 0 && this.height > 0) {
  11181. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11182. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11183. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11184. this.growthIndicator = this.width - width;
  11185. }
  11186. }
  11187. };
  11188. Node.prototype._drawImage = function (ctx) {
  11189. this._resizeImage(ctx);
  11190. this.left = this.x - this.width / 2;
  11191. this.top = this.y - this.height / 2;
  11192. var yLabel;
  11193. if (this.imageObj.width != 0 ) {
  11194. // draw the shade
  11195. if (this.clusterSize > 1) {
  11196. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  11197. lineWidth *= this.networkScaleInv;
  11198. lineWidth = Math.min(0.2 * this.width,lineWidth);
  11199. ctx.globalAlpha = 0.5;
  11200. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  11201. }
  11202. // draw the image
  11203. ctx.globalAlpha = 1.0;
  11204. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  11205. yLabel = this.y + this.height / 2;
  11206. }
  11207. else {
  11208. // image still loading... just draw the label for now
  11209. yLabel = this.y;
  11210. }
  11211. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  11212. };
  11213. Node.prototype._resizeBox = function (ctx) {
  11214. if (!this.width) {
  11215. var margin = 5;
  11216. var textSize = this.getTextSize(ctx);
  11217. this.width = textSize.width + 2 * margin;
  11218. this.height = textSize.height + 2 * margin;
  11219. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  11220. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  11221. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  11222. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  11223. }
  11224. };
  11225. Node.prototype._drawBox = function (ctx) {
  11226. this._resizeBox(ctx);
  11227. this.left = this.x - this.width / 2;
  11228. this.top = this.y - this.height / 2;
  11229. var clusterLineWidth = 2.5;
  11230. var selectionLineWidth = 2;
  11231. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11232. // draw the outer border
  11233. if (this.clusterSize > 1) {
  11234. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11235. ctx.lineWidth *= this.networkScaleInv;
  11236. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11237. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  11238. ctx.stroke();
  11239. }
  11240. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11241. ctx.lineWidth *= this.networkScaleInv;
  11242. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11243. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11244. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  11245. ctx.fill();
  11246. ctx.stroke();
  11247. this._label(ctx, this.label, this.x, this.y);
  11248. };
  11249. Node.prototype._resizeDatabase = function (ctx) {
  11250. if (!this.width) {
  11251. var margin = 5;
  11252. var textSize = this.getTextSize(ctx);
  11253. var size = textSize.width + 2 * margin;
  11254. this.width = size;
  11255. this.height = size;
  11256. // scaling used for clustering
  11257. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11258. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11259. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11260. this.growthIndicator = this.width - size;
  11261. }
  11262. };
  11263. Node.prototype._drawDatabase = function (ctx) {
  11264. this._resizeDatabase(ctx);
  11265. this.left = this.x - this.width / 2;
  11266. this.top = this.y - this.height / 2;
  11267. var clusterLineWidth = 2.5;
  11268. var selectionLineWidth = 2;
  11269. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11270. // draw the outer border
  11271. if (this.clusterSize > 1) {
  11272. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11273. ctx.lineWidth *= this.networkScaleInv;
  11274. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11275. 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);
  11276. ctx.stroke();
  11277. }
  11278. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11279. ctx.lineWidth *= this.networkScaleInv;
  11280. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11281. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11282. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  11283. ctx.fill();
  11284. ctx.stroke();
  11285. this._label(ctx, this.label, this.x, this.y);
  11286. };
  11287. Node.prototype._resizeCircle = function (ctx) {
  11288. if (!this.width) {
  11289. var margin = 5;
  11290. var textSize = this.getTextSize(ctx);
  11291. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  11292. this.radius = diameter / 2;
  11293. this.width = diameter;
  11294. this.height = diameter;
  11295. // scaling used for clustering
  11296. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  11297. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  11298. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  11299. this.growthIndicator = this.radius - 0.5*diameter;
  11300. }
  11301. };
  11302. Node.prototype._drawCircle = function (ctx) {
  11303. this._resizeCircle(ctx);
  11304. this.left = this.x - this.width / 2;
  11305. this.top = this.y - this.height / 2;
  11306. var clusterLineWidth = 2.5;
  11307. var selectionLineWidth = 2;
  11308. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11309. // draw the outer border
  11310. if (this.clusterSize > 1) {
  11311. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11312. ctx.lineWidth *= this.networkScaleInv;
  11313. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11314. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  11315. ctx.stroke();
  11316. }
  11317. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11318. ctx.lineWidth *= this.networkScaleInv;
  11319. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11320. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11321. ctx.circle(this.x, this.y, this.radius);
  11322. ctx.fill();
  11323. ctx.stroke();
  11324. this._label(ctx, this.label, this.x, this.y);
  11325. };
  11326. Node.prototype._resizeEllipse = function (ctx) {
  11327. if (!this.width) {
  11328. var textSize = this.getTextSize(ctx);
  11329. this.width = textSize.width * 1.5;
  11330. this.height = textSize.height * 2;
  11331. if (this.width < this.height) {
  11332. this.width = this.height;
  11333. }
  11334. var defaultSize = this.width;
  11335. // scaling used for clustering
  11336. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11337. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11338. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11339. this.growthIndicator = this.width - defaultSize;
  11340. }
  11341. };
  11342. Node.prototype._drawEllipse = function (ctx) {
  11343. this._resizeEllipse(ctx);
  11344. this.left = this.x - this.width / 2;
  11345. this.top = this.y - this.height / 2;
  11346. var clusterLineWidth = 2.5;
  11347. var selectionLineWidth = 2;
  11348. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11349. // draw the outer border
  11350. if (this.clusterSize > 1) {
  11351. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11352. ctx.lineWidth *= this.networkScaleInv;
  11353. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11354. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  11355. ctx.stroke();
  11356. }
  11357. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11358. ctx.lineWidth *= this.networkScaleInv;
  11359. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11360. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11361. ctx.ellipse(this.left, this.top, this.width, this.height);
  11362. ctx.fill();
  11363. ctx.stroke();
  11364. this._label(ctx, this.label, this.x, this.y);
  11365. };
  11366. Node.prototype._drawDot = function (ctx) {
  11367. this._drawShape(ctx, 'circle');
  11368. };
  11369. Node.prototype._drawTriangle = function (ctx) {
  11370. this._drawShape(ctx, 'triangle');
  11371. };
  11372. Node.prototype._drawTriangleDown = function (ctx) {
  11373. this._drawShape(ctx, 'triangleDown');
  11374. };
  11375. Node.prototype._drawSquare = function (ctx) {
  11376. this._drawShape(ctx, 'square');
  11377. };
  11378. Node.prototype._drawStar = function (ctx) {
  11379. this._drawShape(ctx, 'star');
  11380. };
  11381. Node.prototype._resizeShape = function (ctx) {
  11382. if (!this.width) {
  11383. this.radius = this.baseRadiusValue;
  11384. var size = 2 * this.radius;
  11385. this.width = size;
  11386. this.height = size;
  11387. // scaling used for clustering
  11388. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11389. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11390. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  11391. this.growthIndicator = this.width - size;
  11392. }
  11393. };
  11394. Node.prototype._drawShape = function (ctx, shape) {
  11395. this._resizeShape(ctx);
  11396. this.left = this.x - this.width / 2;
  11397. this.top = this.y - this.height / 2;
  11398. var clusterLineWidth = 2.5;
  11399. var selectionLineWidth = 2;
  11400. var radiusMultiplier = 2;
  11401. // choose draw method depending on the shape
  11402. switch (shape) {
  11403. case 'dot': radiusMultiplier = 2; break;
  11404. case 'square': radiusMultiplier = 2; break;
  11405. case 'triangle': radiusMultiplier = 3; break;
  11406. case 'triangleDown': radiusMultiplier = 3; break;
  11407. case 'star': radiusMultiplier = 4; break;
  11408. }
  11409. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11410. // draw the outer border
  11411. if (this.clusterSize > 1) {
  11412. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11413. ctx.lineWidth *= this.networkScaleInv;
  11414. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11415. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  11416. ctx.stroke();
  11417. }
  11418. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11419. ctx.lineWidth *= this.networkScaleInv;
  11420. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11421. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11422. ctx[shape](this.x, this.y, this.radius);
  11423. ctx.fill();
  11424. ctx.stroke();
  11425. if (this.label) {
  11426. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true);
  11427. }
  11428. };
  11429. Node.prototype._resizeText = function (ctx) {
  11430. if (!this.width) {
  11431. var margin = 5;
  11432. var textSize = this.getTextSize(ctx);
  11433. this.width = textSize.width + 2 * margin;
  11434. this.height = textSize.height + 2 * margin;
  11435. // scaling used for clustering
  11436. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11437. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11438. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11439. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  11440. }
  11441. };
  11442. Node.prototype._drawText = function (ctx) {
  11443. this._resizeText(ctx);
  11444. this.left = this.x - this.width / 2;
  11445. this.top = this.y - this.height / 2;
  11446. this._label(ctx, this.label, this.x, this.y);
  11447. };
  11448. Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
  11449. if (text && this.fontSize * this.networkScale > this.fontDrawThreshold) {
  11450. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  11451. ctx.fillStyle = this.fontColor || "black";
  11452. ctx.textAlign = align || "center";
  11453. ctx.textBaseline = baseline || "middle";
  11454. var lines = text.split('\n');
  11455. var lineCount = lines.length;
  11456. var fontSize = (this.fontSize + 4);
  11457. var yLine = y + (1 - lineCount) / 2 * fontSize;
  11458. if (labelUnderNode == true) {
  11459. yLine = y + (1 - lineCount) / (2 * fontSize);
  11460. }
  11461. for (var i = 0; i < lineCount; i++) {
  11462. ctx.fillText(lines[i], x, yLine);
  11463. yLine += fontSize;
  11464. }
  11465. }
  11466. };
  11467. Node.prototype.getTextSize = function(ctx) {
  11468. if (this.label !== undefined) {
  11469. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  11470. var lines = this.label.split('\n'),
  11471. height = (this.fontSize + 4) * lines.length,
  11472. width = 0;
  11473. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  11474. width = Math.max(width, ctx.measureText(lines[i]).width);
  11475. }
  11476. return {"width": width, "height": height};
  11477. }
  11478. else {
  11479. return {"width": 0, "height": 0};
  11480. }
  11481. };
  11482. /**
  11483. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  11484. * there is a safety margin of 0.3 * width;
  11485. *
  11486. * @returns {boolean}
  11487. */
  11488. Node.prototype.inArea = function() {
  11489. if (this.width !== undefined) {
  11490. return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x &&
  11491. this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x &&
  11492. this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y &&
  11493. this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y);
  11494. }
  11495. else {
  11496. return true;
  11497. }
  11498. };
  11499. /**
  11500. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  11501. * @returns {boolean}
  11502. */
  11503. Node.prototype.inView = function() {
  11504. return (this.x >= this.canvasTopLeft.x &&
  11505. this.x < this.canvasBottomRight.x &&
  11506. this.y >= this.canvasTopLeft.y &&
  11507. this.y < this.canvasBottomRight.y);
  11508. };
  11509. /**
  11510. * This allows the zoom level of the network to influence the rendering
  11511. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  11512. *
  11513. * @param scale
  11514. * @param canvasTopLeft
  11515. * @param canvasBottomRight
  11516. */
  11517. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  11518. this.networkScaleInv = 1.0/scale;
  11519. this.networkScale = scale;
  11520. this.canvasTopLeft = canvasTopLeft;
  11521. this.canvasBottomRight = canvasBottomRight;
  11522. };
  11523. /**
  11524. * This allows the zoom level of the network to influence the rendering
  11525. *
  11526. * @param scale
  11527. */
  11528. Node.prototype.setScale = function(scale) {
  11529. this.networkScaleInv = 1.0/scale;
  11530. this.networkScale = scale;
  11531. };
  11532. /**
  11533. * set the velocity at 0. Is called when this node is contained in another during clustering
  11534. */
  11535. Node.prototype.clearVelocity = function() {
  11536. this.vx = 0;
  11537. this.vy = 0;
  11538. };
  11539. /**
  11540. * Basic preservation of (kinectic) energy
  11541. *
  11542. * @param massBeforeClustering
  11543. */
  11544. Node.prototype.updateVelocity = function(massBeforeClustering) {
  11545. var energyBefore = this.vx * this.vx * massBeforeClustering;
  11546. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  11547. this.vx = Math.sqrt(energyBefore/this.mass);
  11548. energyBefore = this.vy * this.vy * massBeforeClustering;
  11549. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  11550. this.vy = Math.sqrt(energyBefore/this.mass);
  11551. };
  11552. /**
  11553. * @class Edge
  11554. *
  11555. * A edge connects two nodes
  11556. * @param {Object} properties Object with properties. Must contain
  11557. * At least properties from and to.
  11558. * Available properties: from (number),
  11559. * to (number), label (string, color (string),
  11560. * width (number), style (string),
  11561. * length (number), title (string)
  11562. * @param {Network} network A Network object, used to find and edge to
  11563. * nodes.
  11564. * @param {Object} constants An object with default values for
  11565. * example for the color
  11566. */
  11567. function Edge (properties, network, constants) {
  11568. if (!network) {
  11569. throw "No network provided";
  11570. }
  11571. this.network = network;
  11572. // initialize constants
  11573. this.widthMin = constants.edges.widthMin;
  11574. this.widthMax = constants.edges.widthMax;
  11575. // initialize variables
  11576. this.id = undefined;
  11577. this.fromId = undefined;
  11578. this.toId = undefined;
  11579. this.style = constants.edges.style;
  11580. this.title = undefined;
  11581. this.width = constants.edges.width;
  11582. this.widthSelectionMultiplier = constants.edges.widthSelectionMultiplier;
  11583. this.widthSelected = this.width * this.widthSelectionMultiplier;
  11584. this.hoverWidth = constants.edges.hoverWidth;
  11585. this.value = undefined;
  11586. this.length = constants.physics.springLength;
  11587. this.customLength = false;
  11588. this.selected = false;
  11589. this.hover = false;
  11590. this.smooth = constants.smoothCurves;
  11591. this.arrowScaleFactor = constants.edges.arrowScaleFactor;
  11592. this.from = null; // a node
  11593. this.to = null; // a node
  11594. this.via = null; // a temp node
  11595. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  11596. // by storing the original information we can revert to the original connection when the cluser is opened.
  11597. this.originalFromId = [];
  11598. this.originalToId = [];
  11599. this.connected = false;
  11600. // Added to support dashed lines
  11601. // David Jordan
  11602. // 2012-08-08
  11603. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  11604. this.color = {color:constants.edges.color.color,
  11605. highlight:constants.edges.color.highlight,
  11606. hover:constants.edges.color.hover};
  11607. this.widthFixed = false;
  11608. this.lengthFixed = false;
  11609. this.setProperties(properties, constants);
  11610. this.controlNodesEnabled = false;
  11611. this.controlNodes = {from:null, to:null, positions:{}};
  11612. this.connectedNode = null;
  11613. }
  11614. /**
  11615. * Set or overwrite properties for the edge
  11616. * @param {Object} properties an object with properties
  11617. * @param {Object} constants and object with default, global properties
  11618. */
  11619. Edge.prototype.setProperties = function(properties, constants) {
  11620. if (!properties) {
  11621. return;
  11622. }
  11623. if (properties.from !== undefined) {this.fromId = properties.from;}
  11624. if (properties.to !== undefined) {this.toId = properties.to;}
  11625. if (properties.id !== undefined) {this.id = properties.id;}
  11626. if (properties.style !== undefined) {this.style = properties.style;}
  11627. if (properties.label !== undefined) {this.label = properties.label;}
  11628. if (this.label) {
  11629. this.fontSize = constants.edges.fontSize;
  11630. this.fontFace = constants.edges.fontFace;
  11631. this.fontColor = constants.edges.fontColor;
  11632. this.fontFill = constants.edges.fontFill;
  11633. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  11634. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  11635. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  11636. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  11637. }
  11638. if (properties.title !== undefined) {this.title = properties.title;}
  11639. if (properties.width !== undefined) {this.width = properties.width;}
  11640. if (properties.widthSelectionMultiplier !== undefined)
  11641. {this.widthSelectionMultiplier = properties.widthSelectionMultiplier;}
  11642. if (properties.hoverWidth !== undefined) {this.hoverWidth = properties.hoverWidth;}
  11643. if (properties.value !== undefined) {this.value = properties.value;}
  11644. if (properties.length !== undefined) {this.length = properties.length;
  11645. this.customLength = true;}
  11646. // scale the arrow
  11647. if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
  11648. // Added to support dashed lines
  11649. // David Jordan
  11650. // 2012-08-08
  11651. if (properties.dash) {
  11652. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  11653. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  11654. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  11655. }
  11656. if (properties.color !== undefined) {
  11657. if (util.isString(properties.color)) {
  11658. this.color.color = properties.color;
  11659. this.color.highlight = properties.color;
  11660. }
  11661. else {
  11662. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  11663. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  11664. if (properties.color.hover !== undefined) {this.color.hover = properties.color.hover;}
  11665. }
  11666. }
  11667. // A node is connected when it has a from and to node.
  11668. this.connect();
  11669. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  11670. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  11671. this.widthSelected = this.width * this.widthSelectionMultiplier;
  11672. // set draw method based on style
  11673. switch (this.style) {
  11674. case 'line': this.draw = this._drawLine; break;
  11675. case 'arrow': this.draw = this._drawArrow; break;
  11676. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  11677. case 'dash-line': this.draw = this._drawDashLine; break;
  11678. default: this.draw = this._drawLine; break;
  11679. }
  11680. };
  11681. /**
  11682. * Connect an edge to its nodes
  11683. */
  11684. Edge.prototype.connect = function () {
  11685. this.disconnect();
  11686. this.from = this.network.nodes[this.fromId] || null;
  11687. this.to = this.network.nodes[this.toId] || null;
  11688. this.connected = (this.from && this.to);
  11689. if (this.connected) {
  11690. this.from.attachEdge(this);
  11691. this.to.attachEdge(this);
  11692. }
  11693. else {
  11694. if (this.from) {
  11695. this.from.detachEdge(this);
  11696. }
  11697. if (this.to) {
  11698. this.to.detachEdge(this);
  11699. }
  11700. }
  11701. };
  11702. /**
  11703. * Disconnect an edge from its nodes
  11704. */
  11705. Edge.prototype.disconnect = function () {
  11706. if (this.from) {
  11707. this.from.detachEdge(this);
  11708. this.from = null;
  11709. }
  11710. if (this.to) {
  11711. this.to.detachEdge(this);
  11712. this.to = null;
  11713. }
  11714. this.connected = false;
  11715. };
  11716. /**
  11717. * get the title of this edge.
  11718. * @return {string} title The title of the edge, or undefined when no title
  11719. * has been set.
  11720. */
  11721. Edge.prototype.getTitle = function() {
  11722. return typeof this.title === "function" ? this.title() : this.title;
  11723. };
  11724. /**
  11725. * Retrieve the value of the edge. Can be undefined
  11726. * @return {Number} value
  11727. */
  11728. Edge.prototype.getValue = function() {
  11729. return this.value;
  11730. };
  11731. /**
  11732. * Adjust the value range of the edge. The edge will adjust it's width
  11733. * based on its value.
  11734. * @param {Number} min
  11735. * @param {Number} max
  11736. */
  11737. Edge.prototype.setValueRange = function(min, max) {
  11738. if (!this.widthFixed && this.value !== undefined) {
  11739. var scale = (this.widthMax - this.widthMin) / (max - min);
  11740. this.width = (this.value - min) * scale + this.widthMin;
  11741. }
  11742. };
  11743. /**
  11744. * Redraw a edge
  11745. * Draw this edge in the given canvas
  11746. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11747. * @param {CanvasRenderingContext2D} ctx
  11748. */
  11749. Edge.prototype.draw = function(ctx) {
  11750. throw "Method draw not initialized in edge";
  11751. };
  11752. /**
  11753. * Check if this object is overlapping with the provided object
  11754. * @param {Object} obj an object with parameters left, top
  11755. * @return {boolean} True if location is located on the edge
  11756. */
  11757. Edge.prototype.isOverlappingWith = function(obj) {
  11758. if (this.connected) {
  11759. var distMax = 10;
  11760. var xFrom = this.from.x;
  11761. var yFrom = this.from.y;
  11762. var xTo = this.to.x;
  11763. var yTo = this.to.y;
  11764. var xObj = obj.left;
  11765. var yObj = obj.top;
  11766. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  11767. return (dist < distMax);
  11768. }
  11769. else {
  11770. return false
  11771. }
  11772. };
  11773. /**
  11774. * Redraw a edge as a line
  11775. * Draw this edge in the given canvas
  11776. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11777. * @param {CanvasRenderingContext2D} ctx
  11778. * @private
  11779. */
  11780. Edge.prototype._drawLine = function(ctx) {
  11781. // set style
  11782. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  11783. else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
  11784. else {ctx.strokeStyle = this.color.color;}
  11785. ctx.lineWidth = this._getLineWidth();
  11786. if (this.from != this.to) {
  11787. // draw line
  11788. this._line(ctx);
  11789. // draw label
  11790. var point;
  11791. if (this.label) {
  11792. if (this.smooth == true) {
  11793. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  11794. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  11795. point = {x:midpointX, y:midpointY};
  11796. }
  11797. else {
  11798. point = this._pointOnLine(0.5);
  11799. }
  11800. this._label(ctx, this.label, point.x, point.y);
  11801. }
  11802. }
  11803. else {
  11804. var x, y;
  11805. var radius = this.length / 4;
  11806. var node = this.from;
  11807. if (!node.width) {
  11808. node.resize(ctx);
  11809. }
  11810. if (node.width > node.height) {
  11811. x = node.x + node.width / 2;
  11812. y = node.y - radius;
  11813. }
  11814. else {
  11815. x = node.x + radius;
  11816. y = node.y - node.height / 2;
  11817. }
  11818. this._circle(ctx, x, y, radius);
  11819. point = this._pointOnCircle(x, y, radius, 0.5);
  11820. this._label(ctx, this.label, point.x, point.y);
  11821. }
  11822. };
  11823. /**
  11824. * Get the line width of the edge. Depends on width and whether one of the
  11825. * connected nodes is selected.
  11826. * @return {Number} width
  11827. * @private
  11828. */
  11829. Edge.prototype._getLineWidth = function() {
  11830. if (this.selected == true) {
  11831. return Math.min(this.widthSelected, this.widthMax)*this.networkScaleInv;
  11832. }
  11833. else {
  11834. if (this.hover == true) {
  11835. return Math.min(this.hoverWidth, this.widthMax)*this.networkScaleInv;
  11836. }
  11837. else {
  11838. return this.width*this.networkScaleInv;
  11839. }
  11840. }
  11841. };
  11842. /**
  11843. * Draw a line between two nodes
  11844. * @param {CanvasRenderingContext2D} ctx
  11845. * @private
  11846. */
  11847. Edge.prototype._line = function (ctx) {
  11848. // draw a straight line
  11849. ctx.beginPath();
  11850. ctx.moveTo(this.from.x, this.from.y);
  11851. if (this.smooth == true) {
  11852. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  11853. }
  11854. else {
  11855. ctx.lineTo(this.to.x, this.to.y);
  11856. }
  11857. ctx.stroke();
  11858. };
  11859. /**
  11860. * Draw a line from a node to itself, a circle
  11861. * @param {CanvasRenderingContext2D} ctx
  11862. * @param {Number} x
  11863. * @param {Number} y
  11864. * @param {Number} radius
  11865. * @private
  11866. */
  11867. Edge.prototype._circle = function (ctx, x, y, radius) {
  11868. // draw a circle
  11869. ctx.beginPath();
  11870. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  11871. ctx.stroke();
  11872. };
  11873. /**
  11874. * Draw label with white background and with the middle at (x, y)
  11875. * @param {CanvasRenderingContext2D} ctx
  11876. * @param {String} text
  11877. * @param {Number} x
  11878. * @param {Number} y
  11879. * @private
  11880. */
  11881. Edge.prototype._label = function (ctx, text, x, y) {
  11882. if (text) {
  11883. // TODO: cache the calculated size
  11884. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  11885. this.fontSize + "px " + this.fontFace;
  11886. ctx.fillStyle = this.fontFill;
  11887. var width = ctx.measureText(text).width;
  11888. var height = this.fontSize;
  11889. var left = x - width / 2;
  11890. var top = y - height / 2;
  11891. ctx.fillRect(left, top, width, height);
  11892. // draw text
  11893. ctx.fillStyle = this.fontColor || "black";
  11894. ctx.textAlign = "left";
  11895. ctx.textBaseline = "top";
  11896. ctx.fillText(text, left, top);
  11897. }
  11898. };
  11899. /**
  11900. * Redraw a edge as a dashed line
  11901. * Draw this edge in the given canvas
  11902. * @author David Jordan
  11903. * @date 2012-08-08
  11904. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11905. * @param {CanvasRenderingContext2D} ctx
  11906. * @private
  11907. */
  11908. Edge.prototype._drawDashLine = function(ctx) {
  11909. // set style
  11910. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  11911. else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
  11912. else {ctx.strokeStyle = this.color.color;}
  11913. ctx.lineWidth = this._getLineWidth();
  11914. // only firefox and chrome support this method, else we use the legacy one.
  11915. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  11916. ctx.beginPath();
  11917. ctx.moveTo(this.from.x, this.from.y);
  11918. // configure the dash pattern
  11919. var pattern = [0];
  11920. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  11921. pattern = [this.dash.length,this.dash.gap];
  11922. }
  11923. else {
  11924. pattern = [5,5];
  11925. }
  11926. // set dash settings for chrome or firefox
  11927. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  11928. ctx.setLineDash(pattern);
  11929. ctx.lineDashOffset = 0;
  11930. } else { //Firefox
  11931. ctx.mozDash = pattern;
  11932. ctx.mozDashOffset = 0;
  11933. }
  11934. // draw the line
  11935. if (this.smooth == true) {
  11936. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  11937. }
  11938. else {
  11939. ctx.lineTo(this.to.x, this.to.y);
  11940. }
  11941. ctx.stroke();
  11942. // restore the dash settings.
  11943. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  11944. ctx.setLineDash([0]);
  11945. ctx.lineDashOffset = 0;
  11946. } else { //Firefox
  11947. ctx.mozDash = [0];
  11948. ctx.mozDashOffset = 0;
  11949. }
  11950. }
  11951. else { // unsupporting smooth lines
  11952. // draw dashed line
  11953. ctx.beginPath();
  11954. ctx.lineCap = 'round';
  11955. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  11956. {
  11957. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  11958. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  11959. }
  11960. else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
  11961. {
  11962. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  11963. [this.dash.length,this.dash.gap]);
  11964. }
  11965. else //If all else fails draw a line
  11966. {
  11967. ctx.moveTo(this.from.x, this.from.y);
  11968. ctx.lineTo(this.to.x, this.to.y);
  11969. }
  11970. ctx.stroke();
  11971. }
  11972. // draw label
  11973. if (this.label) {
  11974. var point;
  11975. if (this.smooth == true) {
  11976. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  11977. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  11978. point = {x:midpointX, y:midpointY};
  11979. }
  11980. else {
  11981. point = this._pointOnLine(0.5);
  11982. }
  11983. this._label(ctx, this.label, point.x, point.y);
  11984. }
  11985. };
  11986. /**
  11987. * Get a point on a line
  11988. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  11989. * @return {Object} point
  11990. * @private
  11991. */
  11992. Edge.prototype._pointOnLine = function (percentage) {
  11993. return {
  11994. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  11995. y: (1 - percentage) * this.from.y + percentage * this.to.y
  11996. }
  11997. };
  11998. /**
  11999. * Get a point on a circle
  12000. * @param {Number} x
  12001. * @param {Number} y
  12002. * @param {Number} radius
  12003. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  12004. * @return {Object} point
  12005. * @private
  12006. */
  12007. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  12008. var angle = (percentage - 3/8) * 2 * Math.PI;
  12009. return {
  12010. x: x + radius * Math.cos(angle),
  12011. y: y - radius * Math.sin(angle)
  12012. }
  12013. };
  12014. /**
  12015. * Redraw a edge as a line with an arrow halfway the line
  12016. * Draw this edge in the given canvas
  12017. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12018. * @param {CanvasRenderingContext2D} ctx
  12019. * @private
  12020. */
  12021. Edge.prototype._drawArrowCenter = function(ctx) {
  12022. var point;
  12023. // set style
  12024. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  12025. else if (this.hover == true) {ctx.strokeStyle = this.color.hover; ctx.fillStyle = this.color.hover;}
  12026. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  12027. ctx.lineWidth = this._getLineWidth();
  12028. if (this.from != this.to) {
  12029. // draw line
  12030. this._line(ctx);
  12031. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12032. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12033. // draw an arrow halfway the line
  12034. if (this.smooth == true) {
  12035. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  12036. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  12037. point = {x:midpointX, y:midpointY};
  12038. }
  12039. else {
  12040. point = this._pointOnLine(0.5);
  12041. }
  12042. ctx.arrow(point.x, point.y, angle, length);
  12043. ctx.fill();
  12044. ctx.stroke();
  12045. // draw label
  12046. if (this.label) {
  12047. this._label(ctx, this.label, point.x, point.y);
  12048. }
  12049. }
  12050. else {
  12051. // draw circle
  12052. var x, y;
  12053. var radius = 0.25 * Math.max(100,this.length);
  12054. var node = this.from;
  12055. if (!node.width) {
  12056. node.resize(ctx);
  12057. }
  12058. if (node.width > node.height) {
  12059. x = node.x + node.width * 0.5;
  12060. y = node.y - radius;
  12061. }
  12062. else {
  12063. x = node.x + radius;
  12064. y = node.y - node.height * 0.5;
  12065. }
  12066. this._circle(ctx, x, y, radius);
  12067. // draw all arrows
  12068. var angle = 0.2 * Math.PI;
  12069. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12070. point = this._pointOnCircle(x, y, radius, 0.5);
  12071. ctx.arrow(point.x, point.y, angle, length);
  12072. ctx.fill();
  12073. ctx.stroke();
  12074. // draw label
  12075. if (this.label) {
  12076. point = this._pointOnCircle(x, y, radius, 0.5);
  12077. this._label(ctx, this.label, point.x, point.y);
  12078. }
  12079. }
  12080. };
  12081. /**
  12082. * Redraw a edge as a line with an arrow
  12083. * Draw this edge in the given canvas
  12084. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12085. * @param {CanvasRenderingContext2D} ctx
  12086. * @private
  12087. */
  12088. Edge.prototype._drawArrow = function(ctx) {
  12089. // set style
  12090. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  12091. else if (this.hover == true) {ctx.strokeStyle = this.color.hover; ctx.fillStyle = this.color.hover;}
  12092. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  12093. ctx.lineWidth = this._getLineWidth();
  12094. var angle, length;
  12095. //draw a line
  12096. if (this.from != this.to) {
  12097. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12098. var dx = (this.to.x - this.from.x);
  12099. var dy = (this.to.y - this.from.y);
  12100. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12101. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  12102. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  12103. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  12104. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  12105. if (this.smooth == true) {
  12106. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  12107. dx = (this.to.x - this.via.x);
  12108. dy = (this.to.y - this.via.y);
  12109. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12110. }
  12111. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  12112. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  12113. var xTo,yTo;
  12114. if (this.smooth == true) {
  12115. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  12116. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  12117. }
  12118. else {
  12119. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  12120. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  12121. }
  12122. ctx.beginPath();
  12123. ctx.moveTo(xFrom,yFrom);
  12124. if (this.smooth == true) {
  12125. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  12126. }
  12127. else {
  12128. ctx.lineTo(xTo, yTo);
  12129. }
  12130. ctx.stroke();
  12131. // draw arrow at the end of the line
  12132. length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12133. ctx.arrow(xTo, yTo, angle, length);
  12134. ctx.fill();
  12135. ctx.stroke();
  12136. // draw label
  12137. if (this.label) {
  12138. var point;
  12139. if (this.smooth == true) {
  12140. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  12141. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  12142. point = {x:midpointX, y:midpointY};
  12143. }
  12144. else {
  12145. point = this._pointOnLine(0.5);
  12146. }
  12147. this._label(ctx, this.label, point.x, point.y);
  12148. }
  12149. }
  12150. else {
  12151. // draw circle
  12152. var node = this.from;
  12153. var x, y, arrow;
  12154. var radius = 0.25 * Math.max(100,this.length);
  12155. if (!node.width) {
  12156. node.resize(ctx);
  12157. }
  12158. if (node.width > node.height) {
  12159. x = node.x + node.width * 0.5;
  12160. y = node.y - radius;
  12161. arrow = {
  12162. x: x,
  12163. y: node.y,
  12164. angle: 0.9 * Math.PI
  12165. };
  12166. }
  12167. else {
  12168. x = node.x + radius;
  12169. y = node.y - node.height * 0.5;
  12170. arrow = {
  12171. x: node.x,
  12172. y: y,
  12173. angle: 0.6 * Math.PI
  12174. };
  12175. }
  12176. ctx.beginPath();
  12177. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  12178. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  12179. ctx.stroke();
  12180. // draw all arrows
  12181. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12182. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  12183. ctx.fill();
  12184. ctx.stroke();
  12185. // draw label
  12186. if (this.label) {
  12187. point = this._pointOnCircle(x, y, radius, 0.5);
  12188. this._label(ctx, this.label, point.x, point.y);
  12189. }
  12190. }
  12191. };
  12192. /**
  12193. * Calculate the distance between a point (x3,y3) and a line segment from
  12194. * (x1,y1) to (x2,y2).
  12195. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  12196. * @param {number} x1
  12197. * @param {number} y1
  12198. * @param {number} x2
  12199. * @param {number} y2
  12200. * @param {number} x3
  12201. * @param {number} y3
  12202. * @private
  12203. */
  12204. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  12205. if (this.from != this.to) {
  12206. if (this.smooth == true) {
  12207. var minDistance = 1e9;
  12208. var i,t,x,y,dx,dy;
  12209. for (i = 0; i < 10; i++) {
  12210. t = 0.1*i;
  12211. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  12212. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  12213. dx = Math.abs(x3-x);
  12214. dy = Math.abs(y3-y);
  12215. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  12216. }
  12217. return minDistance
  12218. }
  12219. else {
  12220. var px = x2-x1,
  12221. py = y2-y1,
  12222. something = px*px + py*py,
  12223. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  12224. if (u > 1) {
  12225. u = 1;
  12226. }
  12227. else if (u < 0) {
  12228. u = 0;
  12229. }
  12230. var x = x1 + u * px,
  12231. y = y1 + u * py,
  12232. dx = x - x3,
  12233. dy = y - y3;
  12234. //# Note: If the actual distance does not matter,
  12235. //# if you only want to compare what this function
  12236. //# returns to other results of this function, you
  12237. //# can just return the squared distance instead
  12238. //# (i.e. remove the sqrt) to gain a little performance
  12239. return Math.sqrt(dx*dx + dy*dy);
  12240. }
  12241. }
  12242. else {
  12243. var x, y, dx, dy;
  12244. var radius = this.length / 4;
  12245. var node = this.from;
  12246. if (!node.width) {
  12247. node.resize(ctx);
  12248. }
  12249. if (node.width > node.height) {
  12250. x = node.x + node.width / 2;
  12251. y = node.y - radius;
  12252. }
  12253. else {
  12254. x = node.x + radius;
  12255. y = node.y - node.height / 2;
  12256. }
  12257. dx = x - x3;
  12258. dy = y - y3;
  12259. return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
  12260. }
  12261. };
  12262. /**
  12263. * This allows the zoom level of the network to influence the rendering
  12264. *
  12265. * @param scale
  12266. */
  12267. Edge.prototype.setScale = function(scale) {
  12268. this.networkScaleInv = 1.0/scale;
  12269. };
  12270. Edge.prototype.select = function() {
  12271. this.selected = true;
  12272. };
  12273. Edge.prototype.unselect = function() {
  12274. this.selected = false;
  12275. };
  12276. Edge.prototype.positionBezierNode = function() {
  12277. if (this.via !== null) {
  12278. this.via.x = 0.5 * (this.from.x + this.to.x);
  12279. this.via.y = 0.5 * (this.from.y + this.to.y);
  12280. }
  12281. };
  12282. /**
  12283. * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true.
  12284. * @param ctx
  12285. */
  12286. Edge.prototype._drawControlNodes = function(ctx) {
  12287. if (this.controlNodesEnabled == true) {
  12288. if (this.controlNodes.from === null && this.controlNodes.to === null) {
  12289. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  12290. var nodeIdTo = "edgeIdTo:".concat(this.id);
  12291. var constants = {
  12292. nodes:{group:'', radius:8},
  12293. physics:{damping:0},
  12294. clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
  12295. };
  12296. this.controlNodes.from = new Node(
  12297. {id:nodeIdFrom,
  12298. shape:'dot',
  12299. color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
  12300. },{},{},constants);
  12301. this.controlNodes.to = new Node(
  12302. {id:nodeIdTo,
  12303. shape:'dot',
  12304. color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
  12305. },{},{},constants);
  12306. }
  12307. if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
  12308. this.controlNodes.positions = this.getControlNodePositions(ctx);
  12309. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  12310. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  12311. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  12312. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  12313. }
  12314. this.controlNodes.from.draw(ctx);
  12315. this.controlNodes.to.draw(ctx);
  12316. }
  12317. else {
  12318. this.controlNodes = {from:null, to:null, positions:{}};
  12319. }
  12320. }
  12321. /**
  12322. * Enable control nodes.
  12323. * @private
  12324. */
  12325. Edge.prototype._enableControlNodes = function() {
  12326. this.controlNodesEnabled = true;
  12327. }
  12328. /**
  12329. * disable control nodes
  12330. * @private
  12331. */
  12332. Edge.prototype._disableControlNodes = function() {
  12333. this.controlNodesEnabled = false;
  12334. }
  12335. /**
  12336. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
  12337. * @param x
  12338. * @param y
  12339. * @returns {null}
  12340. * @private
  12341. */
  12342. Edge.prototype._getSelectedControlNode = function(x,y) {
  12343. var positions = this.controlNodes.positions;
  12344. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
  12345. var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
  12346. if (fromDistance < 15) {
  12347. this.connectedNode = this.from;
  12348. this.from = this.controlNodes.from;
  12349. return this.controlNodes.from;
  12350. }
  12351. else if (toDistance < 15) {
  12352. this.connectedNode = this.to;
  12353. this.to = this.controlNodes.to;
  12354. return this.controlNodes.to;
  12355. }
  12356. else {
  12357. return null;
  12358. }
  12359. }
  12360. /**
  12361. * this resets the control nodes to their original position.
  12362. * @private
  12363. */
  12364. Edge.prototype._restoreControlNodes = function() {
  12365. if (this.controlNodes.from.selected == true) {
  12366. this.from = this.connectedNode;
  12367. this.connectedNode = null;
  12368. this.controlNodes.from.unselect();
  12369. }
  12370. if (this.controlNodes.to.selected == true) {
  12371. this.to = this.connectedNode;
  12372. this.connectedNode = null;
  12373. this.controlNodes.to.unselect();
  12374. }
  12375. }
  12376. /**
  12377. * this calculates the position of the control nodes on the edges of the parent nodes.
  12378. *
  12379. * @param ctx
  12380. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  12381. */
  12382. Edge.prototype.getControlNodePositions = function(ctx) {
  12383. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12384. var dx = (this.to.x - this.from.x);
  12385. var dy = (this.to.y - this.from.y);
  12386. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12387. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  12388. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  12389. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  12390. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  12391. if (this.smooth == true) {
  12392. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  12393. dx = (this.to.x - this.via.x);
  12394. dy = (this.to.y - this.via.y);
  12395. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12396. }
  12397. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  12398. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  12399. var xTo,yTo;
  12400. if (this.smooth == true) {
  12401. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  12402. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  12403. }
  12404. else {
  12405. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  12406. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  12407. }
  12408. return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
  12409. }
  12410. /**
  12411. * Popup is a class to create a popup window with some text
  12412. * @param {Element} container The container object.
  12413. * @param {Number} [x]
  12414. * @param {Number} [y]
  12415. * @param {String} [text]
  12416. * @param {Object} [style] An object containing borderColor,
  12417. * backgroundColor, etc.
  12418. */
  12419. function Popup(container, x, y, text, style) {
  12420. if (container) {
  12421. this.container = container;
  12422. }
  12423. else {
  12424. this.container = document.body;
  12425. }
  12426. // x, y and text are optional, see if a style object was passed in their place
  12427. if (style === undefined) {
  12428. if (typeof x === "object") {
  12429. style = x;
  12430. x = undefined;
  12431. } else if (typeof text === "object") {
  12432. style = text;
  12433. text = undefined;
  12434. } else {
  12435. // for backwards compatibility, in case clients other than Network are creating Popup directly
  12436. style = {
  12437. fontColor: 'black',
  12438. fontSize: 14, // px
  12439. fontFace: 'verdana',
  12440. color: {
  12441. border: '#666',
  12442. background: '#FFFFC6'
  12443. }
  12444. }
  12445. }
  12446. }
  12447. this.x = 0;
  12448. this.y = 0;
  12449. this.padding = 5;
  12450. if (x !== undefined && y !== undefined ) {
  12451. this.setPosition(x, y);
  12452. }
  12453. if (text !== undefined) {
  12454. this.setText(text);
  12455. }
  12456. // create the frame
  12457. this.frame = document.createElement("div");
  12458. var styleAttr = this.frame.style;
  12459. styleAttr.position = "absolute";
  12460. styleAttr.visibility = "hidden";
  12461. styleAttr.border = "1px solid " + style.color.border;
  12462. styleAttr.color = style.fontColor;
  12463. styleAttr.fontSize = style.fontSize + "px";
  12464. styleAttr.fontFamily = style.fontFace;
  12465. styleAttr.padding = this.padding + "px";
  12466. styleAttr.backgroundColor = style.color.background;
  12467. styleAttr.borderRadius = "3px";
  12468. styleAttr.MozBorderRadius = "3px";
  12469. styleAttr.WebkitBorderRadius = "3px";
  12470. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  12471. styleAttr.whiteSpace = "nowrap";
  12472. this.container.appendChild(this.frame);
  12473. }
  12474. /**
  12475. * @param {number} x Horizontal position of the popup window
  12476. * @param {number} y Vertical position of the popup window
  12477. */
  12478. Popup.prototype.setPosition = function(x, y) {
  12479. this.x = parseInt(x);
  12480. this.y = parseInt(y);
  12481. };
  12482. /**
  12483. * Set the text for the popup window. This can be HTML code
  12484. * @param {string} text
  12485. */
  12486. Popup.prototype.setText = function(text) {
  12487. this.frame.innerHTML = text;
  12488. };
  12489. /**
  12490. * Show the popup window
  12491. * @param {boolean} show Optional. Show or hide the window
  12492. */
  12493. Popup.prototype.show = function (show) {
  12494. if (show === undefined) {
  12495. show = true;
  12496. }
  12497. if (show) {
  12498. var height = this.frame.clientHeight;
  12499. var width = this.frame.clientWidth;
  12500. var maxHeight = this.frame.parentNode.clientHeight;
  12501. var maxWidth = this.frame.parentNode.clientWidth;
  12502. var top = (this.y - height);
  12503. if (top + height + this.padding > maxHeight) {
  12504. top = maxHeight - height - this.padding;
  12505. }
  12506. if (top < this.padding) {
  12507. top = this.padding;
  12508. }
  12509. var left = this.x;
  12510. if (left + width + this.padding > maxWidth) {
  12511. left = maxWidth - width - this.padding;
  12512. }
  12513. if (left < this.padding) {
  12514. left = this.padding;
  12515. }
  12516. this.frame.style.left = left + "px";
  12517. this.frame.style.top = top + "px";
  12518. this.frame.style.visibility = "visible";
  12519. }
  12520. else {
  12521. this.hide();
  12522. }
  12523. };
  12524. /**
  12525. * Hide the popup window
  12526. */
  12527. Popup.prototype.hide = function () {
  12528. this.frame.style.visibility = "hidden";
  12529. };
  12530. /**
  12531. * @class Groups
  12532. * This class can store groups and properties specific for groups.
  12533. */
  12534. function Groups() {
  12535. this.clear();
  12536. this.defaultIndex = 0;
  12537. }
  12538. /**
  12539. * default constants for group colors
  12540. */
  12541. Groups.DEFAULT = [
  12542. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  12543. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  12544. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  12545. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  12546. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  12547. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  12548. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  12549. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  12550. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  12551. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  12552. ];
  12553. /**
  12554. * Clear all groups
  12555. */
  12556. Groups.prototype.clear = function () {
  12557. this.groups = {};
  12558. this.groups.length = function()
  12559. {
  12560. var i = 0;
  12561. for ( var p in this ) {
  12562. if (this.hasOwnProperty(p)) {
  12563. i++;
  12564. }
  12565. }
  12566. return i;
  12567. }
  12568. };
  12569. /**
  12570. * get group properties of a groupname. If groupname is not found, a new group
  12571. * is added.
  12572. * @param {*} groupname Can be a number, string, Date, etc.
  12573. * @return {Object} group The created group, containing all group properties
  12574. */
  12575. Groups.prototype.get = function (groupname) {
  12576. var group = this.groups[groupname];
  12577. if (group == undefined) {
  12578. // create new group
  12579. var index = this.defaultIndex % Groups.DEFAULT.length;
  12580. this.defaultIndex++;
  12581. group = {};
  12582. group.color = Groups.DEFAULT[index];
  12583. this.groups[groupname] = group;
  12584. }
  12585. return group;
  12586. };
  12587. /**
  12588. * Add a custom group style
  12589. * @param {String} groupname
  12590. * @param {Object} style An object containing borderColor,
  12591. * backgroundColor, etc.
  12592. * @return {Object} group The created group object
  12593. */
  12594. Groups.prototype.add = function (groupname, style) {
  12595. this.groups[groupname] = style;
  12596. if (style.color) {
  12597. style.color = util.parseColor(style.color);
  12598. }
  12599. return style;
  12600. };
  12601. /**
  12602. * @class Images
  12603. * This class loads images and keeps them stored.
  12604. */
  12605. function Images() {
  12606. this.images = {};
  12607. this.callback = undefined;
  12608. }
  12609. /**
  12610. * Set an onload callback function. This will be called each time an image
  12611. * is loaded
  12612. * @param {function} callback
  12613. */
  12614. Images.prototype.setOnloadCallback = function(callback) {
  12615. this.callback = callback;
  12616. };
  12617. /**
  12618. *
  12619. * @param {string} url Url of the image
  12620. * @return {Image} img The image object
  12621. */
  12622. Images.prototype.load = function(url) {
  12623. var img = this.images[url];
  12624. if (img == undefined) {
  12625. // create the image
  12626. var images = this;
  12627. img = new Image();
  12628. this.images[url] = img;
  12629. img.onload = function() {
  12630. if (images.callback) {
  12631. images.callback(this);
  12632. }
  12633. };
  12634. img.src = url;
  12635. }
  12636. return img;
  12637. };
  12638. /**
  12639. * Created by Alex on 2/6/14.
  12640. */
  12641. var physicsMixin = {
  12642. /**
  12643. * Toggling barnes Hut calculation on and off.
  12644. *
  12645. * @private
  12646. */
  12647. _toggleBarnesHut: function () {
  12648. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  12649. this._loadSelectedForceSolver();
  12650. this.moving = true;
  12651. this.start();
  12652. },
  12653. /**
  12654. * This loads the node force solver based on the barnes hut or repulsion algorithm
  12655. *
  12656. * @private
  12657. */
  12658. _loadSelectedForceSolver: function () {
  12659. // this overloads the this._calculateNodeForces
  12660. if (this.constants.physics.barnesHut.enabled == true) {
  12661. this._clearMixin(repulsionMixin);
  12662. this._clearMixin(hierarchalRepulsionMixin);
  12663. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  12664. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  12665. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  12666. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  12667. this._loadMixin(barnesHutMixin);
  12668. }
  12669. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  12670. this._clearMixin(barnesHutMixin);
  12671. this._clearMixin(repulsionMixin);
  12672. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  12673. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  12674. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  12675. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  12676. this._loadMixin(hierarchalRepulsionMixin);
  12677. }
  12678. else {
  12679. this._clearMixin(barnesHutMixin);
  12680. this._clearMixin(hierarchalRepulsionMixin);
  12681. this.barnesHutTree = undefined;
  12682. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  12683. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  12684. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  12685. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  12686. this._loadMixin(repulsionMixin);
  12687. }
  12688. },
  12689. /**
  12690. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  12691. * if there is more than one node. If it is just one node, we dont calculate anything.
  12692. *
  12693. * @private
  12694. */
  12695. _initializeForceCalculation: function () {
  12696. // stop calculation if there is only one node
  12697. if (this.nodeIndices.length == 1) {
  12698. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  12699. }
  12700. else {
  12701. // if there are too many nodes on screen, we cluster without repositioning
  12702. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  12703. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  12704. }
  12705. // we now start the force calculation
  12706. this._calculateForces();
  12707. }
  12708. },
  12709. /**
  12710. * Calculate the external forces acting on the nodes
  12711. * Forces are caused by: edges, repulsing forces between nodes, gravity
  12712. * @private
  12713. */
  12714. _calculateForces: function () {
  12715. // Gravity is required to keep separated groups from floating off
  12716. // the forces are reset to zero in this loop by using _setForce instead
  12717. // of _addForce
  12718. this._calculateGravitationalForces();
  12719. this._calculateNodeForces();
  12720. if (this.constants.smoothCurves == true) {
  12721. this._calculateSpringForcesWithSupport();
  12722. }
  12723. else {
  12724. if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  12725. this._calculateHierarchicalSpringForces();
  12726. }
  12727. else {
  12728. this._calculateSpringForces();
  12729. }
  12730. }
  12731. },
  12732. /**
  12733. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  12734. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  12735. * This function joins the datanodes and invisible (called support) nodes into one object.
  12736. * We do this so we do not contaminate this.nodes with the support nodes.
  12737. *
  12738. * @private
  12739. */
  12740. _updateCalculationNodes: function () {
  12741. if (this.constants.smoothCurves == true) {
  12742. this.calculationNodes = {};
  12743. this.calculationNodeIndices = [];
  12744. for (var nodeId in this.nodes) {
  12745. if (this.nodes.hasOwnProperty(nodeId)) {
  12746. this.calculationNodes[nodeId] = this.nodes[nodeId];
  12747. }
  12748. }
  12749. var supportNodes = this.sectors['support']['nodes'];
  12750. for (var supportNodeId in supportNodes) {
  12751. if (supportNodes.hasOwnProperty(supportNodeId)) {
  12752. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  12753. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  12754. }
  12755. else {
  12756. supportNodes[supportNodeId]._setForce(0, 0);
  12757. }
  12758. }
  12759. }
  12760. for (var idx in this.calculationNodes) {
  12761. if (this.calculationNodes.hasOwnProperty(idx)) {
  12762. this.calculationNodeIndices.push(idx);
  12763. }
  12764. }
  12765. }
  12766. else {
  12767. this.calculationNodes = this.nodes;
  12768. this.calculationNodeIndices = this.nodeIndices;
  12769. }
  12770. },
  12771. /**
  12772. * this function applies the central gravity effect to keep groups from floating off
  12773. *
  12774. * @private
  12775. */
  12776. _calculateGravitationalForces: function () {
  12777. var dx, dy, distance, node, i;
  12778. var nodes = this.calculationNodes;
  12779. var gravity = this.constants.physics.centralGravity;
  12780. var gravityForce = 0;
  12781. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  12782. node = nodes[this.calculationNodeIndices[i]];
  12783. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  12784. // gravity does not apply when we are in a pocket sector
  12785. if (this._sector() == "default" && gravity != 0) {
  12786. dx = -node.x;
  12787. dy = -node.y;
  12788. distance = Math.sqrt(dx * dx + dy * dy);
  12789. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  12790. node.fx = dx * gravityForce;
  12791. node.fy = dy * gravityForce;
  12792. }
  12793. else {
  12794. node.fx = 0;
  12795. node.fy = 0;
  12796. }
  12797. }
  12798. },
  12799. /**
  12800. * this function calculates the effects of the springs in the case of unsmooth curves.
  12801. *
  12802. * @private
  12803. */
  12804. _calculateSpringForces: function () {
  12805. var edgeLength, edge, edgeId;
  12806. var dx, dy, fx, fy, springForce, distance;
  12807. var edges = this.edges;
  12808. // forces caused by the edges, modelled as springs
  12809. for (edgeId in edges) {
  12810. if (edges.hasOwnProperty(edgeId)) {
  12811. edge = edges[edgeId];
  12812. if (edge.connected) {
  12813. // only calculate forces if nodes are in the same sector
  12814. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  12815. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  12816. // this implies that the edges between big clusters are longer
  12817. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  12818. dx = (edge.from.x - edge.to.x);
  12819. dy = (edge.from.y - edge.to.y);
  12820. distance = Math.sqrt(dx * dx + dy * dy);
  12821. if (distance == 0) {
  12822. distance = 0.01;
  12823. }
  12824. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  12825. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  12826. fx = dx * springForce;
  12827. fy = dy * springForce;
  12828. edge.from.fx += fx;
  12829. edge.from.fy += fy;
  12830. edge.to.fx -= fx;
  12831. edge.to.fy -= fy;
  12832. }
  12833. }
  12834. }
  12835. }
  12836. },
  12837. /**
  12838. * This function calculates the springforces on the nodes, accounting for the support nodes.
  12839. *
  12840. * @private
  12841. */
  12842. _calculateSpringForcesWithSupport: function () {
  12843. var edgeLength, edge, edgeId, combinedClusterSize;
  12844. var edges = this.edges;
  12845. // forces caused by the edges, modelled as springs
  12846. for (edgeId in edges) {
  12847. if (edges.hasOwnProperty(edgeId)) {
  12848. edge = edges[edgeId];
  12849. if (edge.connected) {
  12850. // only calculate forces if nodes are in the same sector
  12851. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  12852. if (edge.via != null) {
  12853. var node1 = edge.to;
  12854. var node2 = edge.via;
  12855. var node3 = edge.from;
  12856. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  12857. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  12858. // this implies that the edges between big clusters are longer
  12859. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  12860. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  12861. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  12862. }
  12863. }
  12864. }
  12865. }
  12866. }
  12867. },
  12868. /**
  12869. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  12870. *
  12871. * @param node1
  12872. * @param node2
  12873. * @param edgeLength
  12874. * @private
  12875. */
  12876. _calculateSpringForce: function (node1, node2, edgeLength) {
  12877. var dx, dy, fx, fy, springForce, distance;
  12878. dx = (node1.x - node2.x);
  12879. dy = (node1.y - node2.y);
  12880. distance = Math.sqrt(dx * dx + dy * dy);
  12881. if (distance == 0) {
  12882. distance = 0.01;
  12883. }
  12884. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  12885. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  12886. fx = dx * springForce;
  12887. fy = dy * springForce;
  12888. node1.fx += fx;
  12889. node1.fy += fy;
  12890. node2.fx -= fx;
  12891. node2.fy -= fy;
  12892. },
  12893. /**
  12894. * Load the HTML for the physics config and bind it
  12895. * @private
  12896. */
  12897. _loadPhysicsConfiguration: function () {
  12898. if (this.physicsConfiguration === undefined) {
  12899. this.backupConstants = {};
  12900. util.deepExtend(this.backupConstants,this.constants);
  12901. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  12902. this.physicsConfiguration = document.createElement('div');
  12903. this.physicsConfiguration.className = "PhysicsConfiguration";
  12904. this.physicsConfiguration.innerHTML = '' +
  12905. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  12906. '<tr>' +
  12907. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  12908. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  12909. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  12910. '</tr>' +
  12911. '</table>' +
  12912. '<table id="graph_BH_table" style="display:none">' +
  12913. '<tr><td><b>Barnes Hut</b></td></tr>' +
  12914. '<tr>' +
  12915. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
  12916. '</tr>' +
  12917. '<tr>' +
  12918. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
  12919. '</tr>' +
  12920. '<tr>' +
  12921. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
  12922. '</tr>' +
  12923. '<tr>' +
  12924. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
  12925. '</tr>' +
  12926. '<tr>' +
  12927. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
  12928. '</tr>' +
  12929. '</table>' +
  12930. '<table id="graph_R_table" style="display:none">' +
  12931. '<tr><td><b>Repulsion</b></td></tr>' +
  12932. '<tr>' +
  12933. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
  12934. '</tr>' +
  12935. '<tr>' +
  12936. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
  12937. '</tr>' +
  12938. '<tr>' +
  12939. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
  12940. '</tr>' +
  12941. '<tr>' +
  12942. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
  12943. '</tr>' +
  12944. '<tr>' +
  12945. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
  12946. '</tr>' +
  12947. '</table>' +
  12948. '<table id="graph_H_table" style="display:none">' +
  12949. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  12950. '<tr>' +
  12951. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
  12952. '</tr>' +
  12953. '<tr>' +
  12954. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
  12955. '</tr>' +
  12956. '<tr>' +
  12957. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
  12958. '</tr>' +
  12959. '<tr>' +
  12960. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
  12961. '</tr>' +
  12962. '<tr>' +
  12963. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
  12964. '</tr>' +
  12965. '<tr>' +
  12966. '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
  12967. '</tr>' +
  12968. '<tr>' +
  12969. '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
  12970. '</tr>' +
  12971. '<tr>' +
  12972. '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
  12973. '</tr>' +
  12974. '</table>' +
  12975. '<table><tr><td><b>Options:</b></td></tr>' +
  12976. '<tr>' +
  12977. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  12978. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  12979. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  12980. '</tr>' +
  12981. '</table>'
  12982. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  12983. this.optionsDiv = document.createElement("div");
  12984. this.optionsDiv.style.fontSize = "14px";
  12985. this.optionsDiv.style.fontFamily = "verdana";
  12986. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  12987. var rangeElement;
  12988. rangeElement = document.getElementById('graph_BH_gc');
  12989. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  12990. rangeElement = document.getElementById('graph_BH_cg');
  12991. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  12992. rangeElement = document.getElementById('graph_BH_sc');
  12993. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  12994. rangeElement = document.getElementById('graph_BH_sl');
  12995. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  12996. rangeElement = document.getElementById('graph_BH_damp');
  12997. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  12998. rangeElement = document.getElementById('graph_R_nd');
  12999. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  13000. rangeElement = document.getElementById('graph_R_cg');
  13001. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  13002. rangeElement = document.getElementById('graph_R_sc');
  13003. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  13004. rangeElement = document.getElementById('graph_R_sl');
  13005. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  13006. rangeElement = document.getElementById('graph_R_damp');
  13007. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  13008. rangeElement = document.getElementById('graph_H_nd');
  13009. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  13010. rangeElement = document.getElementById('graph_H_cg');
  13011. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  13012. rangeElement = document.getElementById('graph_H_sc');
  13013. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  13014. rangeElement = document.getElementById('graph_H_sl');
  13015. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  13016. rangeElement = document.getElementById('graph_H_damp');
  13017. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  13018. rangeElement = document.getElementById('graph_H_direction');
  13019. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  13020. rangeElement = document.getElementById('graph_H_levsep');
  13021. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  13022. rangeElement = document.getElementById('graph_H_nspac');
  13023. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  13024. var radioButton1 = document.getElementById("graph_physicsMethod1");
  13025. var radioButton2 = document.getElementById("graph_physicsMethod2");
  13026. var radioButton3 = document.getElementById("graph_physicsMethod3");
  13027. radioButton2.checked = true;
  13028. if (this.constants.physics.barnesHut.enabled) {
  13029. radioButton1.checked = true;
  13030. }
  13031. if (this.constants.hierarchicalLayout.enabled) {
  13032. radioButton3.checked = true;
  13033. }
  13034. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  13035. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  13036. var graph_generateOptions = document.getElementById("graph_generateOptions");
  13037. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  13038. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  13039. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  13040. if (this.constants.smoothCurves == true) {
  13041. graph_toggleSmooth.style.background = "#A4FF56";
  13042. }
  13043. else {
  13044. graph_toggleSmooth.style.background = "#FF8532";
  13045. }
  13046. switchConfigurations.apply(this);
  13047. radioButton1.onchange = switchConfigurations.bind(this);
  13048. radioButton2.onchange = switchConfigurations.bind(this);
  13049. radioButton3.onchange = switchConfigurations.bind(this);
  13050. }
  13051. },
  13052. /**
  13053. * This overwrites the this.constants.
  13054. *
  13055. * @param constantsVariableName
  13056. * @param value
  13057. * @private
  13058. */
  13059. _overWriteGraphConstants: function (constantsVariableName, value) {
  13060. var nameArray = constantsVariableName.split("_");
  13061. if (nameArray.length == 1) {
  13062. this.constants[nameArray[0]] = value;
  13063. }
  13064. else if (nameArray.length == 2) {
  13065. this.constants[nameArray[0]][nameArray[1]] = value;
  13066. }
  13067. else if (nameArray.length == 3) {
  13068. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  13069. }
  13070. }
  13071. };
  13072. /**
  13073. * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
  13074. */
  13075. function graphToggleSmoothCurves () {
  13076. this.constants.smoothCurves = !this.constants.smoothCurves;
  13077. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  13078. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  13079. else {graph_toggleSmooth.style.background = "#FF8532";}
  13080. this._configureSmoothCurves(false);
  13081. };
  13082. /**
  13083. * this function is used to scramble the nodes
  13084. *
  13085. */
  13086. function graphRepositionNodes () {
  13087. for (var nodeId in this.calculationNodes) {
  13088. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  13089. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  13090. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  13091. }
  13092. }
  13093. if (this.constants.hierarchicalLayout.enabled == true) {
  13094. this._setupHierarchicalLayout();
  13095. }
  13096. else {
  13097. this.repositionNodes();
  13098. }
  13099. this.moving = true;
  13100. this.start();
  13101. };
  13102. /**
  13103. * this is used to generate an options file from the playing with physics system.
  13104. */
  13105. function graphGenerateOptions () {
  13106. var options = "No options are required, default values used.";
  13107. var optionsSpecific = [];
  13108. var radioButton1 = document.getElementById("graph_physicsMethod1");
  13109. var radioButton2 = document.getElementById("graph_physicsMethod2");
  13110. if (radioButton1.checked == true) {
  13111. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  13112. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  13113. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  13114. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  13115. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  13116. if (optionsSpecific.length != 0) {
  13117. options = "var options = {";
  13118. options += "physics: {barnesHut: {";
  13119. for (var i = 0; i < optionsSpecific.length; i++) {
  13120. options += optionsSpecific[i];
  13121. if (i < optionsSpecific.length - 1) {
  13122. options += ", "
  13123. }
  13124. }
  13125. options += '}}'
  13126. }
  13127. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  13128. if (optionsSpecific.length == 0) {options = "var options = {";}
  13129. else {options += ", "}
  13130. options += "smoothCurves: " + this.constants.smoothCurves;
  13131. }
  13132. if (options != "No options are required, default values used.") {
  13133. options += '};'
  13134. }
  13135. }
  13136. else if (radioButton2.checked == true) {
  13137. options = "var options = {";
  13138. options += "physics: {barnesHut: {enabled: false}";
  13139. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  13140. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  13141. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  13142. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  13143. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  13144. if (optionsSpecific.length != 0) {
  13145. options += ", repulsion: {";
  13146. for (var i = 0; i < optionsSpecific.length; i++) {
  13147. options += optionsSpecific[i];
  13148. if (i < optionsSpecific.length - 1) {
  13149. options += ", "
  13150. }
  13151. }
  13152. options += '}}'
  13153. }
  13154. if (optionsSpecific.length == 0) {options += "}"}
  13155. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  13156. options += ", smoothCurves: " + this.constants.smoothCurves;
  13157. }
  13158. options += '};'
  13159. }
  13160. else {
  13161. options = "var options = {";
  13162. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  13163. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  13164. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  13165. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  13166. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  13167. if (optionsSpecific.length != 0) {
  13168. options += "physics: {hierarchicalRepulsion: {";
  13169. for (var i = 0; i < optionsSpecific.length; i++) {
  13170. options += optionsSpecific[i];
  13171. if (i < optionsSpecific.length - 1) {
  13172. options += ", ";
  13173. }
  13174. }
  13175. options += '}},';
  13176. }
  13177. options += 'hierarchicalLayout: {';
  13178. optionsSpecific = [];
  13179. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  13180. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  13181. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  13182. if (optionsSpecific.length != 0) {
  13183. for (var i = 0; i < optionsSpecific.length; i++) {
  13184. options += optionsSpecific[i];
  13185. if (i < optionsSpecific.length - 1) {
  13186. options += ", "
  13187. }
  13188. }
  13189. options += '}'
  13190. }
  13191. else {
  13192. options += "enabled:true}";
  13193. }
  13194. options += '};'
  13195. }
  13196. this.optionsDiv.innerHTML = options;
  13197. };
  13198. /**
  13199. * this is used to switch between barnesHut, repulsion and hierarchical.
  13200. *
  13201. */
  13202. function switchConfigurations () {
  13203. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  13204. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  13205. var tableId = "graph_" + radioButton + "_table";
  13206. var table = document.getElementById(tableId);
  13207. table.style.display = "block";
  13208. for (var i = 0; i < ids.length; i++) {
  13209. if (ids[i] != tableId) {
  13210. table = document.getElementById(ids[i]);
  13211. table.style.display = "none";
  13212. }
  13213. }
  13214. this._restoreNodes();
  13215. if (radioButton == "R") {
  13216. this.constants.hierarchicalLayout.enabled = false;
  13217. this.constants.physics.hierarchicalRepulsion.enabled = false;
  13218. this.constants.physics.barnesHut.enabled = false;
  13219. }
  13220. else if (radioButton == "H") {
  13221. if (this.constants.hierarchicalLayout.enabled == false) {
  13222. this.constants.hierarchicalLayout.enabled = true;
  13223. this.constants.physics.hierarchicalRepulsion.enabled = true;
  13224. this.constants.physics.barnesHut.enabled = false;
  13225. this._setupHierarchicalLayout();
  13226. }
  13227. }
  13228. else {
  13229. this.constants.hierarchicalLayout.enabled = false;
  13230. this.constants.physics.hierarchicalRepulsion.enabled = false;
  13231. this.constants.physics.barnesHut.enabled = true;
  13232. }
  13233. this._loadSelectedForceSolver();
  13234. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  13235. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  13236. else {graph_toggleSmooth.style.background = "#FF8532";}
  13237. this.moving = true;
  13238. this.start();
  13239. }
  13240. /**
  13241. * this generates the ranges depending on the iniital values.
  13242. *
  13243. * @param id
  13244. * @param map
  13245. * @param constantsVariableName
  13246. */
  13247. function showValueOfRange (id,map,constantsVariableName) {
  13248. var valueId = id + "_value";
  13249. var rangeValue = document.getElementById(id).value;
  13250. if (map instanceof Array) {
  13251. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  13252. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  13253. }
  13254. else {
  13255. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  13256. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  13257. }
  13258. if (constantsVariableName == "hierarchicalLayout_direction" ||
  13259. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  13260. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  13261. this._setupHierarchicalLayout();
  13262. }
  13263. this.moving = true;
  13264. this.start();
  13265. };
  13266. /**
  13267. * Created by Alex on 2/10/14.
  13268. */
  13269. var hierarchalRepulsionMixin = {
  13270. /**
  13271. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  13272. * This field is linearly approximated.
  13273. *
  13274. * @private
  13275. */
  13276. _calculateNodeForces: function () {
  13277. var dx, dy, distance, fx, fy, combinedClusterSize,
  13278. repulsingForce, node1, node2, i, j;
  13279. var nodes = this.calculationNodes;
  13280. var nodeIndices = this.calculationNodeIndices;
  13281. // approximation constants
  13282. var b = 5;
  13283. var a_base = 0.5 * -b;
  13284. // repulsing forces between nodes
  13285. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  13286. var minimumDistance = nodeDistance;
  13287. var a = a_base / minimumDistance;
  13288. // we loop from i over all but the last entree in the array
  13289. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  13290. for (i = 0; i < nodeIndices.length - 1; i++) {
  13291. node1 = nodes[nodeIndices[i]];
  13292. for (j = i + 1; j < nodeIndices.length; j++) {
  13293. node2 = nodes[nodeIndices[j]];
  13294. if (node1.level == node2.level) {
  13295. dx = node2.x - node1.x;
  13296. dy = node2.y - node1.y;
  13297. distance = Math.sqrt(dx * dx + dy * dy);
  13298. if (distance < 2 * minimumDistance) {
  13299. repulsingForce = a * distance + b;
  13300. var c = 0.05;
  13301. var d = 2 * minimumDistance * 2 * c;
  13302. repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c);
  13303. // normalize force with
  13304. if (distance == 0) {
  13305. distance = 0.01;
  13306. }
  13307. else {
  13308. repulsingForce = repulsingForce / distance;
  13309. }
  13310. fx = dx * repulsingForce;
  13311. fy = dy * repulsingForce;
  13312. node1.fx -= fx;
  13313. node1.fy -= fy;
  13314. node2.fx += fx;
  13315. node2.fy += fy;
  13316. }
  13317. }
  13318. }
  13319. }
  13320. },
  13321. /**
  13322. * this function calculates the effects of the springs in the case of unsmooth curves.
  13323. *
  13324. * @private
  13325. */
  13326. _calculateHierarchicalSpringForces: function () {
  13327. var edgeLength, edge, edgeId;
  13328. var dx, dy, fx, fy, springForce, distance;
  13329. var edges = this.edges;
  13330. // forces caused by the edges, modelled as springs
  13331. for (edgeId in edges) {
  13332. if (edges.hasOwnProperty(edgeId)) {
  13333. edge = edges[edgeId];
  13334. if (edge.connected) {
  13335. // only calculate forces if nodes are in the same sector
  13336. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  13337. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  13338. // this implies that the edges between big clusters are longer
  13339. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  13340. dx = (edge.from.x - edge.to.x);
  13341. dy = (edge.from.y - edge.to.y);
  13342. distance = Math.sqrt(dx * dx + dy * dy);
  13343. if (distance == 0) {
  13344. distance = 0.01;
  13345. }
  13346. distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance));
  13347. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  13348. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  13349. fx = dx * springForce;
  13350. fy = dy * springForce;
  13351. edge.to.fx -= fx;
  13352. edge.to.fy -= fy;
  13353. edge.from.fx += fx;
  13354. edge.from.fy += fy;
  13355. var factor = 5;
  13356. if (distance > edgeLength) {
  13357. factor = 25;
  13358. }
  13359. if (edge.from.level > edge.to.level) {
  13360. edge.to.fx -= factor*fx;
  13361. edge.to.fy -= factor*fy;
  13362. }
  13363. else if (edge.from.level < edge.to.level) {
  13364. edge.from.fx += factor*fx;
  13365. edge.from.fy += factor*fy;
  13366. }
  13367. }
  13368. }
  13369. }
  13370. }
  13371. }
  13372. };
  13373. /**
  13374. * Created by Alex on 2/10/14.
  13375. */
  13376. var barnesHutMixin = {
  13377. /**
  13378. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  13379. * The Barnes Hut method is used to speed up this N-body simulation.
  13380. *
  13381. * @private
  13382. */
  13383. _calculateNodeForces : function() {
  13384. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  13385. var node;
  13386. var nodes = this.calculationNodes;
  13387. var nodeIndices = this.calculationNodeIndices;
  13388. var nodeCount = nodeIndices.length;
  13389. this._formBarnesHutTree(nodes,nodeIndices);
  13390. var barnesHutTree = this.barnesHutTree;
  13391. // place the nodes one by one recursively
  13392. for (var i = 0; i < nodeCount; i++) {
  13393. node = nodes[nodeIndices[i]];
  13394. // starting with root is irrelevant, it never passes the BarnesHut condition
  13395. this._getForceContribution(barnesHutTree.root.children.NW,node);
  13396. this._getForceContribution(barnesHutTree.root.children.NE,node);
  13397. this._getForceContribution(barnesHutTree.root.children.SW,node);
  13398. this._getForceContribution(barnesHutTree.root.children.SE,node);
  13399. }
  13400. }
  13401. },
  13402. /**
  13403. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  13404. * If a region contains a single node, we check if it is not itself, then we apply the force.
  13405. *
  13406. * @param parentBranch
  13407. * @param node
  13408. * @private
  13409. */
  13410. _getForceContribution : function(parentBranch,node) {
  13411. // we get no force contribution from an empty region
  13412. if (parentBranch.childrenCount > 0) {
  13413. var dx,dy,distance;
  13414. // get the distance from the center of mass to the node.
  13415. dx = parentBranch.centerOfMass.x - node.x;
  13416. dy = parentBranch.centerOfMass.y - node.y;
  13417. distance = Math.sqrt(dx * dx + dy * dy);
  13418. // BarnesHut condition
  13419. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  13420. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  13421. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  13422. // duplicate code to reduce function calls to speed up program
  13423. if (distance == 0) {
  13424. distance = 0.1*Math.random();
  13425. dx = distance;
  13426. }
  13427. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  13428. var fx = dx * gravityForce;
  13429. var fy = dy * gravityForce;
  13430. node.fx += fx;
  13431. node.fy += fy;
  13432. }
  13433. else {
  13434. // Did not pass the condition, go into children if available
  13435. if (parentBranch.childrenCount == 4) {
  13436. this._getForceContribution(parentBranch.children.NW,node);
  13437. this._getForceContribution(parentBranch.children.NE,node);
  13438. this._getForceContribution(parentBranch.children.SW,node);
  13439. this._getForceContribution(parentBranch.children.SE,node);
  13440. }
  13441. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  13442. if (parentBranch.children.data.id != node.id) { // if it is not self
  13443. // duplicate code to reduce function calls to speed up program
  13444. if (distance == 0) {
  13445. distance = 0.5*Math.random();
  13446. dx = distance;
  13447. }
  13448. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  13449. var fx = dx * gravityForce;
  13450. var fy = dy * gravityForce;
  13451. node.fx += fx;
  13452. node.fy += fy;
  13453. }
  13454. }
  13455. }
  13456. }
  13457. },
  13458. /**
  13459. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  13460. *
  13461. * @param nodes
  13462. * @param nodeIndices
  13463. * @private
  13464. */
  13465. _formBarnesHutTree : function(nodes,nodeIndices) {
  13466. var node;
  13467. var nodeCount = nodeIndices.length;
  13468. var minX = Number.MAX_VALUE,
  13469. minY = Number.MAX_VALUE,
  13470. maxX =-Number.MAX_VALUE,
  13471. maxY =-Number.MAX_VALUE;
  13472. // get the range of the nodes
  13473. for (var i = 0; i < nodeCount; i++) {
  13474. var x = nodes[nodeIndices[i]].x;
  13475. var y = nodes[nodeIndices[i]].y;
  13476. if (x < minX) { minX = x; }
  13477. if (x > maxX) { maxX = x; }
  13478. if (y < minY) { minY = y; }
  13479. if (y > maxY) { maxY = y; }
  13480. }
  13481. // make the range a square
  13482. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  13483. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  13484. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  13485. var minimumTreeSize = 1e-5;
  13486. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  13487. var halfRootSize = 0.5 * rootSize;
  13488. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  13489. // construct the barnesHutTree
  13490. var barnesHutTree = {root:{
  13491. centerOfMass:{x:0,y:0}, // Center of Mass
  13492. mass:0,
  13493. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  13494. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  13495. size: rootSize,
  13496. calcSize: 1 / rootSize,
  13497. children: {data:null},
  13498. maxWidth: 0,
  13499. level: 0,
  13500. childrenCount: 4
  13501. }};
  13502. this._splitBranch(barnesHutTree.root);
  13503. // place the nodes one by one recursively
  13504. for (i = 0; i < nodeCount; i++) {
  13505. node = nodes[nodeIndices[i]];
  13506. this._placeInTree(barnesHutTree.root,node);
  13507. }
  13508. // make global
  13509. this.barnesHutTree = barnesHutTree
  13510. },
  13511. /**
  13512. * this updates the mass of a branch. this is increased by adding a node.
  13513. *
  13514. * @param parentBranch
  13515. * @param node
  13516. * @private
  13517. */
  13518. _updateBranchMass : function(parentBranch, node) {
  13519. var totalMass = parentBranch.mass + node.mass;
  13520. var totalMassInv = 1/totalMass;
  13521. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  13522. parentBranch.centerOfMass.x *= totalMassInv;
  13523. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  13524. parentBranch.centerOfMass.y *= totalMassInv;
  13525. parentBranch.mass = totalMass;
  13526. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  13527. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  13528. },
  13529. /**
  13530. * determine in which branch the node will be placed.
  13531. *
  13532. * @param parentBranch
  13533. * @param node
  13534. * @param skipMassUpdate
  13535. * @private
  13536. */
  13537. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  13538. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  13539. // update the mass of the branch.
  13540. this._updateBranchMass(parentBranch,node);
  13541. }
  13542. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  13543. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  13544. this._placeInRegion(parentBranch,node,"NW");
  13545. }
  13546. else { // in SW
  13547. this._placeInRegion(parentBranch,node,"SW");
  13548. }
  13549. }
  13550. else { // in NE or SE
  13551. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  13552. this._placeInRegion(parentBranch,node,"NE");
  13553. }
  13554. else { // in SE
  13555. this._placeInRegion(parentBranch,node,"SE");
  13556. }
  13557. }
  13558. },
  13559. /**
  13560. * actually place the node in a region (or branch)
  13561. *
  13562. * @param parentBranch
  13563. * @param node
  13564. * @param region
  13565. * @private
  13566. */
  13567. _placeInRegion : function(parentBranch,node,region) {
  13568. switch (parentBranch.children[region].childrenCount) {
  13569. case 0: // place node here
  13570. parentBranch.children[region].children.data = node;
  13571. parentBranch.children[region].childrenCount = 1;
  13572. this._updateBranchMass(parentBranch.children[region],node);
  13573. break;
  13574. case 1: // convert into children
  13575. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  13576. // we move one node a pixel and we do not put it in the tree.
  13577. if (parentBranch.children[region].children.data.x == node.x &&
  13578. parentBranch.children[region].children.data.y == node.y) {
  13579. node.x += Math.random();
  13580. node.y += Math.random();
  13581. }
  13582. else {
  13583. this._splitBranch(parentBranch.children[region]);
  13584. this._placeInTree(parentBranch.children[region],node);
  13585. }
  13586. break;
  13587. case 4: // place in branch
  13588. this._placeInTree(parentBranch.children[region],node);
  13589. break;
  13590. }
  13591. },
  13592. /**
  13593. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  13594. * after the split is complete.
  13595. *
  13596. * @param parentBranch
  13597. * @private
  13598. */
  13599. _splitBranch : function(parentBranch) {
  13600. // if the branch is shaded with a node, replace the node in the new subset.
  13601. var containedNode = null;
  13602. if (parentBranch.childrenCount == 1) {
  13603. containedNode = parentBranch.children.data;
  13604. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  13605. }
  13606. parentBranch.childrenCount = 4;
  13607. parentBranch.children.data = null;
  13608. this._insertRegion(parentBranch,"NW");
  13609. this._insertRegion(parentBranch,"NE");
  13610. this._insertRegion(parentBranch,"SW");
  13611. this._insertRegion(parentBranch,"SE");
  13612. if (containedNode != null) {
  13613. this._placeInTree(parentBranch,containedNode);
  13614. }
  13615. },
  13616. /**
  13617. * This function subdivides the region into four new segments.
  13618. * Specifically, this inserts a single new segment.
  13619. * It fills the children section of the parentBranch
  13620. *
  13621. * @param parentBranch
  13622. * @param region
  13623. * @param parentRange
  13624. * @private
  13625. */
  13626. _insertRegion : function(parentBranch, region) {
  13627. var minX,maxX,minY,maxY;
  13628. var childSize = 0.5 * parentBranch.size;
  13629. switch (region) {
  13630. case "NW":
  13631. minX = parentBranch.range.minX;
  13632. maxX = parentBranch.range.minX + childSize;
  13633. minY = parentBranch.range.minY;
  13634. maxY = parentBranch.range.minY + childSize;
  13635. break;
  13636. case "NE":
  13637. minX = parentBranch.range.minX + childSize;
  13638. maxX = parentBranch.range.maxX;
  13639. minY = parentBranch.range.minY;
  13640. maxY = parentBranch.range.minY + childSize;
  13641. break;
  13642. case "SW":
  13643. minX = parentBranch.range.minX;
  13644. maxX = parentBranch.range.minX + childSize;
  13645. minY = parentBranch.range.minY + childSize;
  13646. maxY = parentBranch.range.maxY;
  13647. break;
  13648. case "SE":
  13649. minX = parentBranch.range.minX + childSize;
  13650. maxX = parentBranch.range.maxX;
  13651. minY = parentBranch.range.minY + childSize;
  13652. maxY = parentBranch.range.maxY;
  13653. break;
  13654. }
  13655. parentBranch.children[region] = {
  13656. centerOfMass:{x:0,y:0},
  13657. mass:0,
  13658. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  13659. size: 0.5 * parentBranch.size,
  13660. calcSize: 2 * parentBranch.calcSize,
  13661. children: {data:null},
  13662. maxWidth: 0,
  13663. level: parentBranch.level+1,
  13664. childrenCount: 0
  13665. };
  13666. },
  13667. /**
  13668. * This function is for debugging purposed, it draws the tree.
  13669. *
  13670. * @param ctx
  13671. * @param color
  13672. * @private
  13673. */
  13674. _drawTree : function(ctx,color) {
  13675. if (this.barnesHutTree !== undefined) {
  13676. ctx.lineWidth = 1;
  13677. this._drawBranch(this.barnesHutTree.root,ctx,color);
  13678. }
  13679. },
  13680. /**
  13681. * This function is for debugging purposes. It draws the branches recursively.
  13682. *
  13683. * @param branch
  13684. * @param ctx
  13685. * @param color
  13686. * @private
  13687. */
  13688. _drawBranch : function(branch,ctx,color) {
  13689. if (color === undefined) {
  13690. color = "#FF0000";
  13691. }
  13692. if (branch.childrenCount == 4) {
  13693. this._drawBranch(branch.children.NW,ctx);
  13694. this._drawBranch(branch.children.NE,ctx);
  13695. this._drawBranch(branch.children.SE,ctx);
  13696. this._drawBranch(branch.children.SW,ctx);
  13697. }
  13698. ctx.strokeStyle = color;
  13699. ctx.beginPath();
  13700. ctx.moveTo(branch.range.minX,branch.range.minY);
  13701. ctx.lineTo(branch.range.maxX,branch.range.minY);
  13702. ctx.stroke();
  13703. ctx.beginPath();
  13704. ctx.moveTo(branch.range.maxX,branch.range.minY);
  13705. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  13706. ctx.stroke();
  13707. ctx.beginPath();
  13708. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  13709. ctx.lineTo(branch.range.minX,branch.range.maxY);
  13710. ctx.stroke();
  13711. ctx.beginPath();
  13712. ctx.moveTo(branch.range.minX,branch.range.maxY);
  13713. ctx.lineTo(branch.range.minX,branch.range.minY);
  13714. ctx.stroke();
  13715. /*
  13716. if (branch.mass > 0) {
  13717. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  13718. ctx.stroke();
  13719. }
  13720. */
  13721. }
  13722. };
  13723. /**
  13724. * Created by Alex on 2/10/14.
  13725. */
  13726. var repulsionMixin = {
  13727. /**
  13728. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  13729. * This field is linearly approximated.
  13730. *
  13731. * @private
  13732. */
  13733. _calculateNodeForces: function () {
  13734. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  13735. repulsingForce, node1, node2, i, j;
  13736. var nodes = this.calculationNodes;
  13737. var nodeIndices = this.calculationNodeIndices;
  13738. // approximation constants
  13739. var a_base = -2 / 3;
  13740. var b = 4 / 3;
  13741. // repulsing forces between nodes
  13742. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  13743. var minimumDistance = nodeDistance;
  13744. // we loop from i over all but the last entree in the array
  13745. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  13746. for (i = 0; i < nodeIndices.length - 1; i++) {
  13747. node1 = nodes[nodeIndices[i]];
  13748. for (j = i + 1; j < nodeIndices.length; j++) {
  13749. node2 = nodes[nodeIndices[j]];
  13750. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  13751. dx = node2.x - node1.x;
  13752. dy = node2.y - node1.y;
  13753. distance = Math.sqrt(dx * dx + dy * dy);
  13754. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  13755. var a = a_base / minimumDistance;
  13756. if (distance < 2 * minimumDistance) {
  13757. if (distance < 0.5 * minimumDistance) {
  13758. repulsingForce = 1.0;
  13759. }
  13760. else {
  13761. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  13762. }
  13763. // amplify the repulsion for clusters.
  13764. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  13765. repulsingForce = repulsingForce / distance;
  13766. fx = dx * repulsingForce;
  13767. fy = dy * repulsingForce;
  13768. node1.fx -= fx;
  13769. node1.fy -= fy;
  13770. node2.fx += fx;
  13771. node2.fy += fy;
  13772. }
  13773. }
  13774. }
  13775. }
  13776. };
  13777. var HierarchicalLayoutMixin = {
  13778. _resetLevels : function() {
  13779. for (var nodeId in this.nodes) {
  13780. if (this.nodes.hasOwnProperty(nodeId)) {
  13781. var node = this.nodes[nodeId];
  13782. if (node.preassignedLevel == false) {
  13783. node.level = -1;
  13784. }
  13785. }
  13786. }
  13787. },
  13788. /**
  13789. * This is the main function to layout the nodes in a hierarchical way.
  13790. * It checks if the node details are supplied correctly
  13791. *
  13792. * @private
  13793. */
  13794. _setupHierarchicalLayout : function() {
  13795. if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
  13796. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  13797. this.constants.hierarchicalLayout.levelSeparation *= -1;
  13798. }
  13799. else {
  13800. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  13801. }
  13802. // get the size of the largest hubs and check if the user has defined a level for a node.
  13803. var hubsize = 0;
  13804. var node, nodeId;
  13805. var definedLevel = false;
  13806. var undefinedLevel = false;
  13807. for (nodeId in this.nodes) {
  13808. if (this.nodes.hasOwnProperty(nodeId)) {
  13809. node = this.nodes[nodeId];
  13810. if (node.level != -1) {
  13811. definedLevel = true;
  13812. }
  13813. else {
  13814. undefinedLevel = true;
  13815. }
  13816. if (hubsize < node.edges.length) {
  13817. hubsize = node.edges.length;
  13818. }
  13819. }
  13820. }
  13821. // if the user defined some levels but not all, alert and run without hierarchical layout
  13822. if (undefinedLevel == true && definedLevel == true) {
  13823. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  13824. this.zoomExtent(true,this.constants.clustering.enabled);
  13825. if (!this.constants.clustering.enabled) {
  13826. this.start();
  13827. }
  13828. }
  13829. else {
  13830. // setup the system to use hierarchical method.
  13831. this._changeConstants();
  13832. // define levels if undefined by the users. Based on hubsize
  13833. if (undefinedLevel == true) {
  13834. this._determineLevels(hubsize);
  13835. }
  13836. // check the distribution of the nodes per level.
  13837. var distribution = this._getDistribution();
  13838. // place the nodes on the canvas. This also stablilizes the system.
  13839. this._placeNodesByHierarchy(distribution);
  13840. // start the simulation.
  13841. this.start();
  13842. }
  13843. }
  13844. },
  13845. /**
  13846. * This function places the nodes on the canvas based on the hierarchial distribution.
  13847. *
  13848. * @param {Object} distribution | obtained by the function this._getDistribution()
  13849. * @private
  13850. */
  13851. _placeNodesByHierarchy : function(distribution) {
  13852. var nodeId, node;
  13853. // start placing all the level 0 nodes first. Then recursively position their branches.
  13854. for (nodeId in distribution[0].nodes) {
  13855. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  13856. node = distribution[0].nodes[nodeId];
  13857. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  13858. if (node.xFixed) {
  13859. node.x = distribution[0].minPos;
  13860. node.xFixed = false;
  13861. distribution[0].minPos += distribution[0].nodeSpacing;
  13862. }
  13863. }
  13864. else {
  13865. if (node.yFixed) {
  13866. node.y = distribution[0].minPos;
  13867. node.yFixed = false;
  13868. distribution[0].minPos += distribution[0].nodeSpacing;
  13869. }
  13870. }
  13871. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  13872. }
  13873. }
  13874. // stabilize the system after positioning. This function calls zoomExtent.
  13875. this._stabilize();
  13876. },
  13877. /**
  13878. * This function get the distribution of levels based on hubsize
  13879. *
  13880. * @returns {Object}
  13881. * @private
  13882. */
  13883. _getDistribution : function() {
  13884. var distribution = {};
  13885. var nodeId, node, level;
  13886. // 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.
  13887. // the fix of X is removed after the x value has been set.
  13888. for (nodeId in this.nodes) {
  13889. if (this.nodes.hasOwnProperty(nodeId)) {
  13890. node = this.nodes[nodeId];
  13891. node.xFixed = true;
  13892. node.yFixed = true;
  13893. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  13894. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  13895. }
  13896. else {
  13897. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  13898. }
  13899. if (!distribution.hasOwnProperty(node.level)) {
  13900. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  13901. }
  13902. distribution[node.level].amount += 1;
  13903. distribution[node.level].nodes[node.id] = node;
  13904. }
  13905. }
  13906. // determine the largest amount of nodes of all levels
  13907. var maxCount = 0;
  13908. for (level in distribution) {
  13909. if (distribution.hasOwnProperty(level)) {
  13910. if (maxCount < distribution[level].amount) {
  13911. maxCount = distribution[level].amount;
  13912. }
  13913. }
  13914. }
  13915. // set the initial position and spacing of each nodes accordingly
  13916. for (level in distribution) {
  13917. if (distribution.hasOwnProperty(level)) {
  13918. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  13919. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  13920. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  13921. }
  13922. }
  13923. return distribution;
  13924. },
  13925. /**
  13926. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  13927. *
  13928. * @param hubsize
  13929. * @private
  13930. */
  13931. _determineLevels : function(hubsize) {
  13932. var nodeId, node;
  13933. // determine hubs
  13934. for (nodeId in this.nodes) {
  13935. if (this.nodes.hasOwnProperty(nodeId)) {
  13936. node = this.nodes[nodeId];
  13937. if (node.edges.length == hubsize) {
  13938. node.level = 0;
  13939. }
  13940. }
  13941. }
  13942. // branch from hubs
  13943. for (nodeId in this.nodes) {
  13944. if (this.nodes.hasOwnProperty(nodeId)) {
  13945. node = this.nodes[nodeId];
  13946. if (node.level == 0) {
  13947. this._setLevel(1,node.edges,node.id);
  13948. }
  13949. }
  13950. }
  13951. },
  13952. /**
  13953. * Since hierarchical layout does not support:
  13954. * - smooth curves (based on the physics),
  13955. * - clustering (based on dynamic node counts)
  13956. *
  13957. * We disable both features so there will be no problems.
  13958. *
  13959. * @private
  13960. */
  13961. _changeConstants : function() {
  13962. this.constants.clustering.enabled = false;
  13963. this.constants.physics.barnesHut.enabled = false;
  13964. this.constants.physics.hierarchicalRepulsion.enabled = true;
  13965. this._loadSelectedForceSolver();
  13966. this.constants.smoothCurves = false;
  13967. this._configureSmoothCurves();
  13968. },
  13969. /**
  13970. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  13971. * on a X position that ensures there will be no overlap.
  13972. *
  13973. * @param edges
  13974. * @param parentId
  13975. * @param distribution
  13976. * @param parentLevel
  13977. * @private
  13978. */
  13979. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  13980. for (var i = 0; i < edges.length; i++) {
  13981. var childNode = null;
  13982. if (edges[i].toId == parentId) {
  13983. childNode = edges[i].from;
  13984. }
  13985. else {
  13986. childNode = edges[i].to;
  13987. }
  13988. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  13989. var nodeMoved = false;
  13990. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  13991. if (childNode.xFixed && childNode.level > parentLevel) {
  13992. childNode.xFixed = false;
  13993. childNode.x = distribution[childNode.level].minPos;
  13994. nodeMoved = true;
  13995. }
  13996. }
  13997. else {
  13998. if (childNode.yFixed && childNode.level > parentLevel) {
  13999. childNode.yFixed = false;
  14000. childNode.y = distribution[childNode.level].minPos;
  14001. nodeMoved = true;
  14002. }
  14003. }
  14004. if (nodeMoved == true) {
  14005. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  14006. if (childNode.edges.length > 1) {
  14007. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  14008. }
  14009. }
  14010. }
  14011. },
  14012. /**
  14013. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  14014. *
  14015. * @param level
  14016. * @param edges
  14017. * @param parentId
  14018. * @private
  14019. */
  14020. _setLevel : function(level, edges, parentId) {
  14021. for (var i = 0; i < edges.length; i++) {
  14022. var childNode = null;
  14023. if (edges[i].toId == parentId) {
  14024. childNode = edges[i].from;
  14025. }
  14026. else {
  14027. childNode = edges[i].to;
  14028. }
  14029. if (childNode.level == -1 || childNode.level > level) {
  14030. childNode.level = level;
  14031. if (edges.length > 1) {
  14032. this._setLevel(level+1, childNode.edges, childNode.id);
  14033. }
  14034. }
  14035. }
  14036. },
  14037. /**
  14038. * Unfix nodes
  14039. *
  14040. * @private
  14041. */
  14042. _restoreNodes : function() {
  14043. for (nodeId in this.nodes) {
  14044. if (this.nodes.hasOwnProperty(nodeId)) {
  14045. this.nodes[nodeId].xFixed = false;
  14046. this.nodes[nodeId].yFixed = false;
  14047. }
  14048. }
  14049. }
  14050. };
  14051. /**
  14052. * Created by Alex on 2/4/14.
  14053. */
  14054. var manipulationMixin = {
  14055. /**
  14056. * clears the toolbar div element of children
  14057. *
  14058. * @private
  14059. */
  14060. _clearManipulatorBar : function() {
  14061. while (this.manipulationDiv.hasChildNodes()) {
  14062. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  14063. }
  14064. },
  14065. /**
  14066. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  14067. * these functions to their original functionality, we saved them in this.cachedFunctions.
  14068. * This function restores these functions to their original function.
  14069. *
  14070. * @private
  14071. */
  14072. _restoreOverloadedFunctions : function() {
  14073. for (var functionName in this.cachedFunctions) {
  14074. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  14075. this[functionName] = this.cachedFunctions[functionName];
  14076. }
  14077. }
  14078. },
  14079. /**
  14080. * Enable or disable edit-mode.
  14081. *
  14082. * @private
  14083. */
  14084. _toggleEditMode : function() {
  14085. this.editMode = !this.editMode;
  14086. var toolbar = document.getElementById("network-manipulationDiv");
  14087. var closeDiv = document.getElementById("network-manipulation-closeDiv");
  14088. var editModeDiv = document.getElementById("network-manipulation-editMode");
  14089. if (this.editMode == true) {
  14090. toolbar.style.display="block";
  14091. closeDiv.style.display="block";
  14092. editModeDiv.style.display="none";
  14093. closeDiv.onclick = this._toggleEditMode.bind(this);
  14094. }
  14095. else {
  14096. toolbar.style.display="none";
  14097. closeDiv.style.display="none";
  14098. editModeDiv.style.display="block";
  14099. closeDiv.onclick = null;
  14100. }
  14101. this._createManipulatorBar()
  14102. },
  14103. /**
  14104. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  14105. *
  14106. * @private
  14107. */
  14108. _createManipulatorBar : function() {
  14109. // remove bound functions
  14110. if (this.boundFunction) {
  14111. this.off('select', this.boundFunction);
  14112. }
  14113. if (this.edgeBeingEdited !== undefined) {
  14114. this.edgeBeingEdited._disableControlNodes();
  14115. this.edgeBeingEdited = undefined;
  14116. this.selectedControlNode = null;
  14117. }
  14118. // restore overloaded functions
  14119. this._restoreOverloadedFunctions();
  14120. // resume calculation
  14121. this.freezeSimulation = false;
  14122. // reset global variables
  14123. this.blockConnectingEdgeSelection = false;
  14124. this.forceAppendSelection = false;
  14125. if (this.editMode == true) {
  14126. while (this.manipulationDiv.hasChildNodes()) {
  14127. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  14128. }
  14129. // add the icons to the manipulator div
  14130. this.manipulationDiv.innerHTML = "" +
  14131. "<span class='network-manipulationUI add' id='network-manipulate-addNode'>" +
  14132. "<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  14133. "<div class='network-seperatorLine'></div>" +
  14134. "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
  14135. "<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  14136. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  14137. this.manipulationDiv.innerHTML += "" +
  14138. "<div class='network-seperatorLine'></div>" +
  14139. "<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" +
  14140. "<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  14141. }
  14142. else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
  14143. this.manipulationDiv.innerHTML += "" +
  14144. "<div class='network-seperatorLine'></div>" +
  14145. "<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" +
  14146. "<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>";
  14147. }
  14148. if (this._selectionIsEmpty() == false) {
  14149. this.manipulationDiv.innerHTML += "" +
  14150. "<div class='network-seperatorLine'></div>" +
  14151. "<span class='network-manipulationUI delete' id='network-manipulate-delete'>" +
  14152. "<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  14153. }
  14154. // bind the icons
  14155. var addNodeButton = document.getElementById("network-manipulate-addNode");
  14156. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  14157. var addEdgeButton = document.getElementById("network-manipulate-connectNode");
  14158. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  14159. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  14160. var editButton = document.getElementById("network-manipulate-editNode");
  14161. editButton.onclick = this._editNode.bind(this);
  14162. }
  14163. else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
  14164. var editButton = document.getElementById("network-manipulate-editEdge");
  14165. editButton.onclick = this._createEditEdgeToolbar.bind(this);
  14166. }
  14167. if (this._selectionIsEmpty() == false) {
  14168. var deleteButton = document.getElementById("network-manipulate-delete");
  14169. deleteButton.onclick = this._deleteSelected.bind(this);
  14170. }
  14171. var closeDiv = document.getElementById("network-manipulation-closeDiv");
  14172. closeDiv.onclick = this._toggleEditMode.bind(this);
  14173. this.boundFunction = this._createManipulatorBar.bind(this);
  14174. this.on('select', this.boundFunction);
  14175. }
  14176. else {
  14177. this.editModeDiv.innerHTML = "" +
  14178. "<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" +
  14179. "<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  14180. var editModeButton = document.getElementById("network-manipulate-editModeButton");
  14181. editModeButton.onclick = this._toggleEditMode.bind(this);
  14182. }
  14183. },
  14184. /**
  14185. * Create the toolbar for adding Nodes
  14186. *
  14187. * @private
  14188. */
  14189. _createAddNodeToolbar : function() {
  14190. // clear the toolbar
  14191. this._clearManipulatorBar();
  14192. if (this.boundFunction) {
  14193. this.off('select', this.boundFunction);
  14194. }
  14195. // create the toolbar contents
  14196. this.manipulationDiv.innerHTML = "" +
  14197. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  14198. "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  14199. "<div class='network-seperatorLine'></div>" +
  14200. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  14201. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  14202. // bind the icon
  14203. var backButton = document.getElementById("network-manipulate-back");
  14204. backButton.onclick = this._createManipulatorBar.bind(this);
  14205. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  14206. this.boundFunction = this._addNode.bind(this);
  14207. this.on('select', this.boundFunction);
  14208. },
  14209. /**
  14210. * create the toolbar to connect nodes
  14211. *
  14212. * @private
  14213. */
  14214. _createAddEdgeToolbar : function() {
  14215. // clear the toolbar
  14216. this._clearManipulatorBar();
  14217. this._unselectAll(true);
  14218. this.freezeSimulation = true;
  14219. if (this.boundFunction) {
  14220. this.off('select', this.boundFunction);
  14221. }
  14222. this._unselectAll();
  14223. this.forceAppendSelection = false;
  14224. this.blockConnectingEdgeSelection = true;
  14225. this.manipulationDiv.innerHTML = "" +
  14226. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  14227. "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  14228. "<div class='network-seperatorLine'></div>" +
  14229. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  14230. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  14231. // bind the icon
  14232. var backButton = document.getElementById("network-manipulate-back");
  14233. backButton.onclick = this._createManipulatorBar.bind(this);
  14234. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  14235. this.boundFunction = this._handleConnect.bind(this);
  14236. this.on('select', this.boundFunction);
  14237. // temporarily overload functions
  14238. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  14239. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  14240. this._handleTouch = this._handleConnect;
  14241. this._handleOnRelease = this._finishConnect;
  14242. // redraw to show the unselect
  14243. this._redraw();
  14244. },
  14245. /**
  14246. * create the toolbar to edit edges
  14247. *
  14248. * @private
  14249. */
  14250. _createEditEdgeToolbar : function() {
  14251. // clear the toolbar
  14252. this._clearManipulatorBar();
  14253. if (this.boundFunction) {
  14254. this.off('select', this.boundFunction);
  14255. }
  14256. this.edgeBeingEdited = this._getSelectedEdge();
  14257. this.edgeBeingEdited._enableControlNodes();
  14258. this.manipulationDiv.innerHTML = "" +
  14259. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  14260. "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  14261. "<div class='network-seperatorLine'></div>" +
  14262. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  14263. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>";
  14264. // bind the icon
  14265. var backButton = document.getElementById("network-manipulate-back");
  14266. backButton.onclick = this._createManipulatorBar.bind(this);
  14267. // temporarily overload functions
  14268. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  14269. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  14270. this.cachedFunctions["_handleTap"] = this._handleTap;
  14271. this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
  14272. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  14273. this._handleTouch = this._selectControlNode;
  14274. this._handleTap = function () {};
  14275. this._handleOnDrag = this._controlNodeDrag;
  14276. this._handleDragStart = function () {}
  14277. this._handleOnRelease = this._releaseControlNode;
  14278. // redraw to show the unselect
  14279. this._redraw();
  14280. },
  14281. /**
  14282. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  14283. * to walk the user through the process.
  14284. *
  14285. * @private
  14286. */
  14287. _selectControlNode : function(pointer) {
  14288. this.edgeBeingEdited.controlNodes.from.unselect();
  14289. this.edgeBeingEdited.controlNodes.to.unselect();
  14290. this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
  14291. if (this.selectedControlNode !== null) {
  14292. this.selectedControlNode.select();
  14293. this.freezeSimulation = true;
  14294. }
  14295. this._redraw();
  14296. },
  14297. /**
  14298. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  14299. * to walk the user through the process.
  14300. *
  14301. * @private
  14302. */
  14303. _controlNodeDrag : function(event) {
  14304. var pointer = this._getPointer(event.gesture.center);
  14305. if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
  14306. this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
  14307. this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
  14308. }
  14309. this._redraw();
  14310. },
  14311. _releaseControlNode : function(pointer) {
  14312. var newNode = this._getNodeAt(pointer);
  14313. if (newNode != null) {
  14314. if (this.edgeBeingEdited.controlNodes.from.selected == true) {
  14315. this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
  14316. this.edgeBeingEdited.controlNodes.from.unselect();
  14317. }
  14318. if (this.edgeBeingEdited.controlNodes.to.selected == true) {
  14319. this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
  14320. this.edgeBeingEdited.controlNodes.to.unselect();
  14321. }
  14322. }
  14323. else {
  14324. this.edgeBeingEdited._restoreControlNodes();
  14325. }
  14326. this.freezeSimulation = false;
  14327. this._redraw();
  14328. },
  14329. /**
  14330. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  14331. * to walk the user through the process.
  14332. *
  14333. * @private
  14334. */
  14335. _handleConnect : function(pointer) {
  14336. if (this._getSelectedNodeCount() == 0) {
  14337. var node = this._getNodeAt(pointer);
  14338. if (node != null) {
  14339. if (node.clusterSize > 1) {
  14340. alert("Cannot create edges to a cluster.")
  14341. }
  14342. else {
  14343. this._selectObject(node,false);
  14344. // create a node the temporary line can look at
  14345. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  14346. this.sectors['support']['nodes']['targetNode'].x = node.x;
  14347. this.sectors['support']['nodes']['targetNode'].y = node.y;
  14348. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  14349. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  14350. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  14351. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  14352. // create a temporary edge
  14353. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  14354. this.edges['connectionEdge'].from = node;
  14355. this.edges['connectionEdge'].connected = true;
  14356. this.edges['connectionEdge'].smooth = true;
  14357. this.edges['connectionEdge'].selected = true;
  14358. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  14359. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  14360. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  14361. this._handleOnDrag = function(event) {
  14362. var pointer = this._getPointer(event.gesture.center);
  14363. this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
  14364. this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  14365. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
  14366. this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  14367. };
  14368. this.moving = true;
  14369. this.start();
  14370. }
  14371. }
  14372. }
  14373. },
  14374. _finishConnect : function(pointer) {
  14375. if (this._getSelectedNodeCount() == 1) {
  14376. // restore the drag function
  14377. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  14378. delete this.cachedFunctions["_handleOnDrag"];
  14379. // remember the edge id
  14380. var connectFromId = this.edges['connectionEdge'].fromId;
  14381. // remove the temporary nodes and edge
  14382. delete this.edges['connectionEdge'];
  14383. delete this.sectors['support']['nodes']['targetNode'];
  14384. delete this.sectors['support']['nodes']['targetViaNode'];
  14385. var node = this._getNodeAt(pointer);
  14386. if (node != null) {
  14387. if (node.clusterSize > 1) {
  14388. alert("Cannot create edges to a cluster.")
  14389. }
  14390. else {
  14391. this._createEdge(connectFromId,node.id);
  14392. this._createManipulatorBar();
  14393. }
  14394. }
  14395. this._unselectAll();
  14396. }
  14397. },
  14398. /**
  14399. * Adds a node on the specified location
  14400. */
  14401. _addNode : function() {
  14402. if (this._selectionIsEmpty() && this.editMode == true) {
  14403. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  14404. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  14405. if (this.triggerFunctions.add) {
  14406. if (this.triggerFunctions.add.length == 2) {
  14407. var me = this;
  14408. this.triggerFunctions.add(defaultData, function(finalizedData) {
  14409. me.nodesData.add(finalizedData);
  14410. me._createManipulatorBar();
  14411. me.moving = true;
  14412. me.start();
  14413. });
  14414. }
  14415. else {
  14416. alert(this.constants.labels['addError']);
  14417. this._createManipulatorBar();
  14418. this.moving = true;
  14419. this.start();
  14420. }
  14421. }
  14422. else {
  14423. this.nodesData.add(defaultData);
  14424. this._createManipulatorBar();
  14425. this.moving = true;
  14426. this.start();
  14427. }
  14428. }
  14429. },
  14430. /**
  14431. * connect two nodes with a new edge.
  14432. *
  14433. * @private
  14434. */
  14435. _createEdge : function(sourceNodeId,targetNodeId) {
  14436. if (this.editMode == true) {
  14437. var defaultData = {from:sourceNodeId, to:targetNodeId};
  14438. if (this.triggerFunctions.connect) {
  14439. if (this.triggerFunctions.connect.length == 2) {
  14440. var me = this;
  14441. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  14442. me.edgesData.add(finalizedData);
  14443. me.moving = true;
  14444. me.start();
  14445. });
  14446. }
  14447. else {
  14448. alert(this.constants.labels["linkError"]);
  14449. this.moving = true;
  14450. this.start();
  14451. }
  14452. }
  14453. else {
  14454. this.edgesData.add(defaultData);
  14455. this.moving = true;
  14456. this.start();
  14457. }
  14458. }
  14459. },
  14460. /**
  14461. * connect two nodes with a new edge.
  14462. *
  14463. * @private
  14464. */
  14465. _editEdge : function(sourceNodeId,targetNodeId) {
  14466. if (this.editMode == true) {
  14467. var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
  14468. if (this.triggerFunctions.editEdge) {
  14469. if (this.triggerFunctions.editEdge.length == 2) {
  14470. var me = this;
  14471. this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
  14472. me.edgesData.update(finalizedData);
  14473. me.moving = true;
  14474. me.start();
  14475. });
  14476. }
  14477. else {
  14478. alert(this.constants.labels["linkError"]);
  14479. this.moving = true;
  14480. this.start();
  14481. }
  14482. }
  14483. else {
  14484. this.edgesData.update(defaultData);
  14485. this.moving = true;
  14486. this.start();
  14487. }
  14488. }
  14489. },
  14490. /**
  14491. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  14492. *
  14493. * @private
  14494. */
  14495. _editNode : function() {
  14496. if (this.triggerFunctions.edit && this.editMode == true) {
  14497. var node = this._getSelectedNode();
  14498. var data = {id:node.id,
  14499. label: node.label,
  14500. group: node.group,
  14501. shape: node.shape,
  14502. color: {
  14503. background:node.color.background,
  14504. border:node.color.border,
  14505. highlight: {
  14506. background:node.color.highlight.background,
  14507. border:node.color.highlight.border
  14508. }
  14509. }};
  14510. if (this.triggerFunctions.edit.length == 2) {
  14511. var me = this;
  14512. this.triggerFunctions.edit(data, function (finalizedData) {
  14513. me.nodesData.update(finalizedData);
  14514. me._createManipulatorBar();
  14515. me.moving = true;
  14516. me.start();
  14517. });
  14518. }
  14519. else {
  14520. alert(this.constants.labels["editError"]);
  14521. }
  14522. }
  14523. else {
  14524. alert(this.constants.labels["editBoundError"]);
  14525. }
  14526. },
  14527. /**
  14528. * delete everything in the selection
  14529. *
  14530. * @private
  14531. */
  14532. _deleteSelected : function() {
  14533. if (!this._selectionIsEmpty() && this.editMode == true) {
  14534. if (!this._clusterInSelection()) {
  14535. var selectedNodes = this.getSelectedNodes();
  14536. var selectedEdges = this.getSelectedEdges();
  14537. if (this.triggerFunctions.del) {
  14538. var me = this;
  14539. var data = {nodes: selectedNodes, edges: selectedEdges};
  14540. if (this.triggerFunctions.del.length = 2) {
  14541. this.triggerFunctions.del(data, function (finalizedData) {
  14542. me.edgesData.remove(finalizedData.edges);
  14543. me.nodesData.remove(finalizedData.nodes);
  14544. me._unselectAll();
  14545. me.moving = true;
  14546. me.start();
  14547. });
  14548. }
  14549. else {
  14550. alert(this.constants.labels["deleteError"])
  14551. }
  14552. }
  14553. else {
  14554. this.edgesData.remove(selectedEdges);
  14555. this.nodesData.remove(selectedNodes);
  14556. this._unselectAll();
  14557. this.moving = true;
  14558. this.start();
  14559. }
  14560. }
  14561. else {
  14562. alert(this.constants.labels["deleteClusterError"]);
  14563. }
  14564. }
  14565. }
  14566. };
  14567. /**
  14568. * Creation of the SectorMixin var.
  14569. *
  14570. * This contains all the functions the Network object can use to employ the sector system.
  14571. * The sector system is always used by Network, though the benefits only apply to the use of clustering.
  14572. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  14573. *
  14574. * Alex de Mulder
  14575. * 21-01-2013
  14576. */
  14577. var SectorMixin = {
  14578. /**
  14579. * This function is only called by the setData function of the Network object.
  14580. * This loads the global references into the active sector. This initializes the sector.
  14581. *
  14582. * @private
  14583. */
  14584. _putDataInSector : function() {
  14585. this.sectors["active"][this._sector()].nodes = this.nodes;
  14586. this.sectors["active"][this._sector()].edges = this.edges;
  14587. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  14588. },
  14589. /**
  14590. * /**
  14591. * This function sets the global references to nodes, edges and nodeIndices back to
  14592. * those of the supplied (active) sector. If a type is defined, do the specific type
  14593. *
  14594. * @param {String} sectorId
  14595. * @param {String} [sectorType] | "active" or "frozen"
  14596. * @private
  14597. */
  14598. _switchToSector : function(sectorId, sectorType) {
  14599. if (sectorType === undefined || sectorType == "active") {
  14600. this._switchToActiveSector(sectorId);
  14601. }
  14602. else {
  14603. this._switchToFrozenSector(sectorId);
  14604. }
  14605. },
  14606. /**
  14607. * This function sets the global references to nodes, edges and nodeIndices back to
  14608. * those of the supplied active sector.
  14609. *
  14610. * @param sectorId
  14611. * @private
  14612. */
  14613. _switchToActiveSector : function(sectorId) {
  14614. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  14615. this.nodes = this.sectors["active"][sectorId]["nodes"];
  14616. this.edges = this.sectors["active"][sectorId]["edges"];
  14617. },
  14618. /**
  14619. * This function sets the global references to nodes, edges and nodeIndices back to
  14620. * those of the supplied active sector.
  14621. *
  14622. * @param sectorId
  14623. * @private
  14624. */
  14625. _switchToSupportSector : function() {
  14626. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  14627. this.nodes = this.sectors["support"]["nodes"];
  14628. this.edges = this.sectors["support"]["edges"];
  14629. },
  14630. /**
  14631. * This function sets the global references to nodes, edges and nodeIndices back to
  14632. * those of the supplied frozen sector.
  14633. *
  14634. * @param sectorId
  14635. * @private
  14636. */
  14637. _switchToFrozenSector : function(sectorId) {
  14638. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  14639. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  14640. this.edges = this.sectors["frozen"][sectorId]["edges"];
  14641. },
  14642. /**
  14643. * This function sets the global references to nodes, edges and nodeIndices back to
  14644. * those of the currently active sector.
  14645. *
  14646. * @private
  14647. */
  14648. _loadLatestSector : function() {
  14649. this._switchToSector(this._sector());
  14650. },
  14651. /**
  14652. * This function returns the currently active sector Id
  14653. *
  14654. * @returns {String}
  14655. * @private
  14656. */
  14657. _sector : function() {
  14658. return this.activeSector[this.activeSector.length-1];
  14659. },
  14660. /**
  14661. * This function returns the previously active sector Id
  14662. *
  14663. * @returns {String}
  14664. * @private
  14665. */
  14666. _previousSector : function() {
  14667. if (this.activeSector.length > 1) {
  14668. return this.activeSector[this.activeSector.length-2];
  14669. }
  14670. else {
  14671. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  14672. }
  14673. },
  14674. /**
  14675. * We add the active sector at the end of the this.activeSector array
  14676. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  14677. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  14678. *
  14679. * @param newId
  14680. * @private
  14681. */
  14682. _setActiveSector : function(newId) {
  14683. this.activeSector.push(newId);
  14684. },
  14685. /**
  14686. * We remove the currently active sector id from the active sector stack. This happens when
  14687. * we reactivate the previously active sector
  14688. *
  14689. * @private
  14690. */
  14691. _forgetLastSector : function() {
  14692. this.activeSector.pop();
  14693. },
  14694. /**
  14695. * This function creates a new active sector with the supplied newId. This newId
  14696. * is the expanding node id.
  14697. *
  14698. * @param {String} newId | Id of the new active sector
  14699. * @private
  14700. */
  14701. _createNewSector : function(newId) {
  14702. // create the new sector
  14703. this.sectors["active"][newId] = {"nodes":{},
  14704. "edges":{},
  14705. "nodeIndices":[],
  14706. "formationScale": this.scale,
  14707. "drawingNode": undefined};
  14708. // create the new sector render node. This gives visual feedback that you are in a new sector.
  14709. this.sectors["active"][newId]['drawingNode'] = new Node(
  14710. {id:newId,
  14711. color: {
  14712. background: "#eaefef",
  14713. border: "495c5e"
  14714. }
  14715. },{},{},this.constants);
  14716. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  14717. },
  14718. /**
  14719. * This function removes the currently active sector. This is called when we create a new
  14720. * active sector.
  14721. *
  14722. * @param {String} sectorId | Id of the active sector that will be removed
  14723. * @private
  14724. */
  14725. _deleteActiveSector : function(sectorId) {
  14726. delete this.sectors["active"][sectorId];
  14727. },
  14728. /**
  14729. * This function removes the currently active sector. This is called when we reactivate
  14730. * the previously active sector.
  14731. *
  14732. * @param {String} sectorId | Id of the active sector that will be removed
  14733. * @private
  14734. */
  14735. _deleteFrozenSector : function(sectorId) {
  14736. delete this.sectors["frozen"][sectorId];
  14737. },
  14738. /**
  14739. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  14740. * We copy the references, then delete the active entree.
  14741. *
  14742. * @param sectorId
  14743. * @private
  14744. */
  14745. _freezeSector : function(sectorId) {
  14746. // we move the set references from the active to the frozen stack.
  14747. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  14748. // we have moved the sector data into the frozen set, we now remove it from the active set
  14749. this._deleteActiveSector(sectorId);
  14750. },
  14751. /**
  14752. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  14753. * object to the "active" object.
  14754. *
  14755. * @param sectorId
  14756. * @private
  14757. */
  14758. _activateSector : function(sectorId) {
  14759. // we move the set references from the frozen to the active stack.
  14760. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  14761. // we have moved the sector data into the active set, we now remove it from the frozen stack
  14762. this._deleteFrozenSector(sectorId);
  14763. },
  14764. /**
  14765. * This function merges the data from the currently active sector with a frozen sector. This is used
  14766. * in the process of reverting back to the previously active sector.
  14767. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  14768. * upon the creation of a new active sector.
  14769. *
  14770. * @param sectorId
  14771. * @private
  14772. */
  14773. _mergeThisWithFrozen : function(sectorId) {
  14774. // copy all nodes
  14775. for (var nodeId in this.nodes) {
  14776. if (this.nodes.hasOwnProperty(nodeId)) {
  14777. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  14778. }
  14779. }
  14780. // copy all edges (if not fully clustered, else there are no edges)
  14781. for (var edgeId in this.edges) {
  14782. if (this.edges.hasOwnProperty(edgeId)) {
  14783. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  14784. }
  14785. }
  14786. // merge the nodeIndices
  14787. for (var i = 0; i < this.nodeIndices.length; i++) {
  14788. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  14789. }
  14790. },
  14791. /**
  14792. * This clusters the sector to one cluster. It was a single cluster before this process started so
  14793. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  14794. *
  14795. * @private
  14796. */
  14797. _collapseThisToSingleCluster : function() {
  14798. this.clusterToFit(1,false);
  14799. },
  14800. /**
  14801. * We create a new active sector from the node that we want to open.
  14802. *
  14803. * @param node
  14804. * @private
  14805. */
  14806. _addSector : function(node) {
  14807. // this is the currently active sector
  14808. var sector = this._sector();
  14809. // // this should allow me to select nodes from a frozen set.
  14810. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  14811. // console.log("the node is part of the active sector");
  14812. // }
  14813. // else {
  14814. // console.log("I dont know what the fuck happened!!");
  14815. // }
  14816. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  14817. delete this.nodes[node.id];
  14818. var unqiueIdentifier = util.randomUUID();
  14819. // we fully freeze the currently active sector
  14820. this._freezeSector(sector);
  14821. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  14822. this._createNewSector(unqiueIdentifier);
  14823. // we add the active sector to the sectors array to be able to revert these steps later on
  14824. this._setActiveSector(unqiueIdentifier);
  14825. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  14826. this._switchToSector(this._sector());
  14827. // finally we add the node we removed from our previous active sector to the new active sector
  14828. this.nodes[node.id] = node;
  14829. },
  14830. /**
  14831. * We close the sector that is currently open and revert back to the one before.
  14832. * If the active sector is the "default" sector, nothing happens.
  14833. *
  14834. * @private
  14835. */
  14836. _collapseSector : function() {
  14837. // the currently active sector
  14838. var sector = this._sector();
  14839. // we cannot collapse the default sector
  14840. if (sector != "default") {
  14841. if ((this.nodeIndices.length == 1) ||
  14842. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  14843. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  14844. var previousSector = this._previousSector();
  14845. // we collapse the sector back to a single cluster
  14846. this._collapseThisToSingleCluster();
  14847. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  14848. // This previous sector is the one we will reactivate
  14849. this._mergeThisWithFrozen(previousSector);
  14850. // the previously active (frozen) sector now has all the data from the currently active sector.
  14851. // we can now delete the active sector.
  14852. this._deleteActiveSector(sector);
  14853. // we activate the previously active (and currently frozen) sector.
  14854. this._activateSector(previousSector);
  14855. // we load the references from the newly active sector into the global references
  14856. this._switchToSector(previousSector);
  14857. // we forget the previously active sector because we reverted to the one before
  14858. this._forgetLastSector();
  14859. // finally, we update the node index list.
  14860. this._updateNodeIndexList();
  14861. // we refresh the list with calulation nodes and calculation node indices.
  14862. this._updateCalculationNodes();
  14863. }
  14864. }
  14865. },
  14866. /**
  14867. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  14868. *
  14869. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14870. * | we dont pass the function itself because then the "this" is the window object
  14871. * | instead of the Network object
  14872. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14873. * @private
  14874. */
  14875. _doInAllActiveSectors : function(runFunction,argument) {
  14876. if (argument === undefined) {
  14877. for (var sector in this.sectors["active"]) {
  14878. if (this.sectors["active"].hasOwnProperty(sector)) {
  14879. // switch the global references to those of this sector
  14880. this._switchToActiveSector(sector);
  14881. this[runFunction]();
  14882. }
  14883. }
  14884. }
  14885. else {
  14886. for (var sector in this.sectors["active"]) {
  14887. if (this.sectors["active"].hasOwnProperty(sector)) {
  14888. // switch the global references to those of this sector
  14889. this._switchToActiveSector(sector);
  14890. var args = Array.prototype.splice.call(arguments, 1);
  14891. if (args.length > 1) {
  14892. this[runFunction](args[0],args[1]);
  14893. }
  14894. else {
  14895. this[runFunction](argument);
  14896. }
  14897. }
  14898. }
  14899. }
  14900. // we revert the global references back to our active sector
  14901. this._loadLatestSector();
  14902. },
  14903. /**
  14904. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  14905. *
  14906. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14907. * | we dont pass the function itself because then the "this" is the window object
  14908. * | instead of the Network object
  14909. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14910. * @private
  14911. */
  14912. _doInSupportSector : function(runFunction,argument) {
  14913. if (argument === undefined) {
  14914. this._switchToSupportSector();
  14915. this[runFunction]();
  14916. }
  14917. else {
  14918. this._switchToSupportSector();
  14919. var args = Array.prototype.splice.call(arguments, 1);
  14920. if (args.length > 1) {
  14921. this[runFunction](args[0],args[1]);
  14922. }
  14923. else {
  14924. this[runFunction](argument);
  14925. }
  14926. }
  14927. // we revert the global references back to our active sector
  14928. this._loadLatestSector();
  14929. },
  14930. /**
  14931. * This runs a function in all frozen sectors. This is used in the _redraw().
  14932. *
  14933. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14934. * | we don't pass the function itself because then the "this" is the window object
  14935. * | instead of the Network object
  14936. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14937. * @private
  14938. */
  14939. _doInAllFrozenSectors : function(runFunction,argument) {
  14940. if (argument === undefined) {
  14941. for (var sector in this.sectors["frozen"]) {
  14942. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  14943. // switch the global references to those of this sector
  14944. this._switchToFrozenSector(sector);
  14945. this[runFunction]();
  14946. }
  14947. }
  14948. }
  14949. else {
  14950. for (var sector in this.sectors["frozen"]) {
  14951. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  14952. // switch the global references to those of this sector
  14953. this._switchToFrozenSector(sector);
  14954. var args = Array.prototype.splice.call(arguments, 1);
  14955. if (args.length > 1) {
  14956. this[runFunction](args[0],args[1]);
  14957. }
  14958. else {
  14959. this[runFunction](argument);
  14960. }
  14961. }
  14962. }
  14963. }
  14964. this._loadLatestSector();
  14965. },
  14966. /**
  14967. * This runs a function in all sectors. This is used in the _redraw().
  14968. *
  14969. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14970. * | we don't pass the function itself because then the "this" is the window object
  14971. * | instead of the Network object
  14972. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14973. * @private
  14974. */
  14975. _doInAllSectors : function(runFunction,argument) {
  14976. var args = Array.prototype.splice.call(arguments, 1);
  14977. if (argument === undefined) {
  14978. this._doInAllActiveSectors(runFunction);
  14979. this._doInAllFrozenSectors(runFunction);
  14980. }
  14981. else {
  14982. if (args.length > 1) {
  14983. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  14984. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  14985. }
  14986. else {
  14987. this._doInAllActiveSectors(runFunction,argument);
  14988. this._doInAllFrozenSectors(runFunction,argument);
  14989. }
  14990. }
  14991. },
  14992. /**
  14993. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  14994. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  14995. *
  14996. * @private
  14997. */
  14998. _clearNodeIndexList : function() {
  14999. var sector = this._sector();
  15000. this.sectors["active"][sector]["nodeIndices"] = [];
  15001. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  15002. },
  15003. /**
  15004. * Draw the encompassing sector node
  15005. *
  15006. * @param ctx
  15007. * @param sectorType
  15008. * @private
  15009. */
  15010. _drawSectorNodes : function(ctx,sectorType) {
  15011. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  15012. for (var sector in this.sectors[sectorType]) {
  15013. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  15014. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  15015. this._switchToSector(sector,sectorType);
  15016. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  15017. for (var nodeId in this.nodes) {
  15018. if (this.nodes.hasOwnProperty(nodeId)) {
  15019. node = this.nodes[nodeId];
  15020. node.resize(ctx);
  15021. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  15022. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  15023. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  15024. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  15025. }
  15026. }
  15027. node = this.sectors[sectorType][sector]["drawingNode"];
  15028. node.x = 0.5 * (maxX + minX);
  15029. node.y = 0.5 * (maxY + minY);
  15030. node.width = 2 * (node.x - minX);
  15031. node.height = 2 * (node.y - minY);
  15032. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  15033. node.setScale(this.scale);
  15034. node._drawCircle(ctx);
  15035. }
  15036. }
  15037. }
  15038. },
  15039. _drawAllSectorNodes : function(ctx) {
  15040. this._drawSectorNodes(ctx,"frozen");
  15041. this._drawSectorNodes(ctx,"active");
  15042. this._loadLatestSector();
  15043. }
  15044. };
  15045. /**
  15046. * Creation of the ClusterMixin var.
  15047. *
  15048. * This contains all the functions the Network object can use to employ clustering
  15049. *
  15050. * Alex de Mulder
  15051. * 21-01-2013
  15052. */
  15053. var ClusterMixin = {
  15054. /**
  15055. * This is only called in the constructor of the network object
  15056. *
  15057. */
  15058. startWithClustering : function() {
  15059. // cluster if the data set is big
  15060. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  15061. // updates the lables after clustering
  15062. this.updateLabels();
  15063. // this is called here because if clusterin is disabled, the start and stabilize are called in
  15064. // the setData function.
  15065. if (this.stabilize) {
  15066. this._stabilize();
  15067. }
  15068. this.start();
  15069. },
  15070. /**
  15071. * This function clusters until the initialMaxNodes has been reached
  15072. *
  15073. * @param {Number} maxNumberOfNodes
  15074. * @param {Boolean} reposition
  15075. */
  15076. clusterToFit : function(maxNumberOfNodes, reposition) {
  15077. var numberOfNodes = this.nodeIndices.length;
  15078. var maxLevels = 50;
  15079. var level = 0;
  15080. // we first cluster the hubs, then we pull in the outliers, repeat
  15081. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  15082. if (level % 3 == 0) {
  15083. this.forceAggregateHubs(true);
  15084. this.normalizeClusterLevels();
  15085. }
  15086. else {
  15087. this.increaseClusterLevel(); // this also includes a cluster normalization
  15088. }
  15089. numberOfNodes = this.nodeIndices.length;
  15090. level += 1;
  15091. }
  15092. // after the clustering we reposition the nodes to reduce the initial chaos
  15093. if (level > 0 && reposition == true) {
  15094. this.repositionNodes();
  15095. }
  15096. this._updateCalculationNodes();
  15097. },
  15098. /**
  15099. * This function can be called to open up a specific cluster. It is only called by
  15100. * It will unpack the cluster back one level.
  15101. *
  15102. * @param node | Node object: cluster to open.
  15103. */
  15104. openCluster : function(node) {
  15105. var isMovingBeforeClustering = this.moving;
  15106. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  15107. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  15108. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  15109. this._addSector(node);
  15110. var level = 0;
  15111. // we decluster until we reach a decent number of nodes
  15112. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  15113. this.decreaseClusterLevel();
  15114. level += 1;
  15115. }
  15116. }
  15117. else {
  15118. this._expandClusterNode(node,false,true);
  15119. // update the index list, dynamic edges and labels
  15120. this._updateNodeIndexList();
  15121. this._updateDynamicEdges();
  15122. this._updateCalculationNodes();
  15123. this.updateLabels();
  15124. }
  15125. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  15126. if (this.moving != isMovingBeforeClustering) {
  15127. this.start();
  15128. }
  15129. },
  15130. /**
  15131. * This calls the updateClustes with default arguments
  15132. */
  15133. updateClustersDefault : function() {
  15134. if (this.constants.clustering.enabled == true) {
  15135. this.updateClusters(0,false,false);
  15136. }
  15137. },
  15138. /**
  15139. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  15140. * be clustered with their connected node. This can be repeated as many times as needed.
  15141. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  15142. */
  15143. increaseClusterLevel : function() {
  15144. this.updateClusters(-1,false,true);
  15145. },
  15146. /**
  15147. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  15148. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  15149. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  15150. */
  15151. decreaseClusterLevel : function() {
  15152. this.updateClusters(1,false,true);
  15153. },
  15154. /**
  15155. * This is the main clustering function. It clusters and declusters on zoom or forced
  15156. * This function clusters on zoom, it can be called with a predefined zoom direction
  15157. * If out, check if we can form clusters, if in, check if we can open clusters.
  15158. * This function is only called from _zoom()
  15159. *
  15160. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  15161. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  15162. * @param {Boolean} force | enabled or disable forcing
  15163. * @param {Boolean} doNotStart | if true do not call start
  15164. *
  15165. */
  15166. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  15167. var isMovingBeforeClustering = this.moving;
  15168. var amountOfNodes = this.nodeIndices.length;
  15169. // on zoom out collapse the sector if the scale is at the level the sector was made
  15170. if (this.previousScale > this.scale && zoomDirection == 0) {
  15171. this._collapseSector();
  15172. }
  15173. // check if we zoom in or out
  15174. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  15175. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  15176. // outer nodes determines if it is being clustered
  15177. this._formClusters(force);
  15178. }
  15179. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  15180. if (force == true) {
  15181. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  15182. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  15183. this._openClusters(recursive,force);
  15184. }
  15185. else {
  15186. // if a cluster takes up a set percentage of the active window
  15187. this._openClustersBySize();
  15188. }
  15189. }
  15190. this._updateNodeIndexList();
  15191. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  15192. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  15193. this._aggregateHubs(force);
  15194. this._updateNodeIndexList();
  15195. }
  15196. // we now reduce chains.
  15197. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  15198. this.handleChains();
  15199. this._updateNodeIndexList();
  15200. }
  15201. this.previousScale = this.scale;
  15202. // rest of the update the index list, dynamic edges and labels
  15203. this._updateDynamicEdges();
  15204. this.updateLabels();
  15205. // if a cluster was formed, we increase the clusterSession
  15206. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  15207. this.clusterSession += 1;
  15208. // if clusters have been made, we normalize the cluster level
  15209. this.normalizeClusterLevels();
  15210. }
  15211. if (doNotStart == false || doNotStart === undefined) {
  15212. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  15213. if (this.moving != isMovingBeforeClustering) {
  15214. this.start();
  15215. }
  15216. }
  15217. this._updateCalculationNodes();
  15218. },
  15219. /**
  15220. * This function handles the chains. It is called on every updateClusters().
  15221. */
  15222. handleChains : function() {
  15223. // after clustering we check how many chains there are
  15224. var chainPercentage = this._getChainFraction();
  15225. if (chainPercentage > this.constants.clustering.chainThreshold) {
  15226. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  15227. }
  15228. },
  15229. /**
  15230. * this functions starts clustering by hubs
  15231. * The minimum hub threshold is set globally
  15232. *
  15233. * @private
  15234. */
  15235. _aggregateHubs : function(force) {
  15236. this._getHubSize();
  15237. this._formClustersByHub(force,false);
  15238. },
  15239. /**
  15240. * This function is fired by keypress. It forces hubs to form.
  15241. *
  15242. */
  15243. forceAggregateHubs : function(doNotStart) {
  15244. var isMovingBeforeClustering = this.moving;
  15245. var amountOfNodes = this.nodeIndices.length;
  15246. this._aggregateHubs(true);
  15247. // update the index list, dynamic edges and labels
  15248. this._updateNodeIndexList();
  15249. this._updateDynamicEdges();
  15250. this.updateLabels();
  15251. // if a cluster was formed, we increase the clusterSession
  15252. if (this.nodeIndices.length != amountOfNodes) {
  15253. this.clusterSession += 1;
  15254. }
  15255. if (doNotStart == false || doNotStart === undefined) {
  15256. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  15257. if (this.moving != isMovingBeforeClustering) {
  15258. this.start();
  15259. }
  15260. }
  15261. },
  15262. /**
  15263. * If a cluster takes up more than a set percentage of the screen, open the cluster
  15264. *
  15265. * @private
  15266. */
  15267. _openClustersBySize : function() {
  15268. for (var nodeId in this.nodes) {
  15269. if (this.nodes.hasOwnProperty(nodeId)) {
  15270. var node = this.nodes[nodeId];
  15271. if (node.inView() == true) {
  15272. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  15273. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  15274. this.openCluster(node);
  15275. }
  15276. }
  15277. }
  15278. }
  15279. },
  15280. /**
  15281. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  15282. * has to be opened based on the current zoom level.
  15283. *
  15284. * @private
  15285. */
  15286. _openClusters : function(recursive,force) {
  15287. for (var i = 0; i < this.nodeIndices.length; i++) {
  15288. var node = this.nodes[this.nodeIndices[i]];
  15289. this._expandClusterNode(node,recursive,force);
  15290. this._updateCalculationNodes();
  15291. }
  15292. },
  15293. /**
  15294. * This function checks if a node has to be opened. This is done by checking the zoom level.
  15295. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  15296. * This recursive behaviour is optional and can be set by the recursive argument.
  15297. *
  15298. * @param {Node} parentNode | to check for cluster and expand
  15299. * @param {Boolean} recursive | enabled or disable recursive calling
  15300. * @param {Boolean} force | enabled or disable forcing
  15301. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  15302. * @private
  15303. */
  15304. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  15305. // first check if node is a cluster
  15306. if (parentNode.clusterSize > 1) {
  15307. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  15308. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  15309. openAll = true;
  15310. }
  15311. recursive = openAll ? true : recursive;
  15312. // if the last child has been added on a smaller scale than current scale decluster
  15313. if (parentNode.formationScale < this.scale || force == true) {
  15314. // we will check if any of the contained child nodes should be removed from the cluster
  15315. for (var containedNodeId in parentNode.containedNodes) {
  15316. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  15317. var childNode = parentNode.containedNodes[containedNodeId];
  15318. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  15319. // the largest cluster is the one that comes from outside
  15320. if (force == true) {
  15321. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  15322. || openAll) {
  15323. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  15324. }
  15325. }
  15326. else {
  15327. if (this._nodeInActiveArea(parentNode)) {
  15328. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  15329. }
  15330. }
  15331. }
  15332. }
  15333. }
  15334. }
  15335. },
  15336. /**
  15337. * ONLY CALLED FROM _expandClusterNode
  15338. *
  15339. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  15340. * the child node from the parent contained_node object and put it back into the global nodes object.
  15341. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  15342. *
  15343. * @param {Node} parentNode | the parent node
  15344. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  15345. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  15346. * With force and recursive both true, the entire cluster is unpacked
  15347. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  15348. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  15349. * @private
  15350. */
  15351. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  15352. var childNode = parentNode.containedNodes[containedNodeId];
  15353. // if child node has been added on smaller scale than current, kick out
  15354. if (childNode.formationScale < this.scale || force == true) {
  15355. // unselect all selected items
  15356. this._unselectAll();
  15357. // put the child node back in the global nodes object
  15358. this.nodes[containedNodeId] = childNode;
  15359. // release the contained edges from this childNode back into the global edges
  15360. this._releaseContainedEdges(parentNode,childNode);
  15361. // reconnect rerouted edges to the childNode
  15362. this._connectEdgeBackToChild(parentNode,childNode);
  15363. // validate all edges in dynamicEdges
  15364. this._validateEdges(parentNode);
  15365. // undo the changes from the clustering operation on the parent node
  15366. parentNode.mass -= childNode.mass;
  15367. parentNode.clusterSize -= childNode.clusterSize;
  15368. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  15369. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  15370. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  15371. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  15372. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  15373. // remove node from the list
  15374. delete parentNode.containedNodes[containedNodeId];
  15375. // check if there are other childs with this clusterSession in the parent.
  15376. var othersPresent = false;
  15377. for (var childNodeId in parentNode.containedNodes) {
  15378. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  15379. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  15380. othersPresent = true;
  15381. break;
  15382. }
  15383. }
  15384. }
  15385. // if there are no others, remove the cluster session from the list
  15386. if (othersPresent == false) {
  15387. parentNode.clusterSessions.pop();
  15388. }
  15389. this._repositionBezierNodes(childNode);
  15390. // this._repositionBezierNodes(parentNode);
  15391. // remove the clusterSession from the child node
  15392. childNode.clusterSession = 0;
  15393. // recalculate the size of the node on the next time the node is rendered
  15394. parentNode.clearSizeCache();
  15395. // restart the simulation to reorganise all nodes
  15396. this.moving = true;
  15397. }
  15398. // check if a further expansion step is possible if recursivity is enabled
  15399. if (recursive == true) {
  15400. this._expandClusterNode(childNode,recursive,force,openAll);
  15401. }
  15402. },
  15403. /**
  15404. * position the bezier nodes at the center of the edges
  15405. *
  15406. * @param node
  15407. * @private
  15408. */
  15409. _repositionBezierNodes : function(node) {
  15410. for (var i = 0; i < node.dynamicEdges.length; i++) {
  15411. node.dynamicEdges[i].positionBezierNode();
  15412. }
  15413. },
  15414. /**
  15415. * This function checks if any nodes at the end of their trees have edges below a threshold length
  15416. * This function is called only from updateClusters()
  15417. * forceLevelCollapse ignores the length of the edge and collapses one level
  15418. * This means that a node with only one edge will be clustered with its connected node
  15419. *
  15420. * @private
  15421. * @param {Boolean} force
  15422. */
  15423. _formClusters : function(force) {
  15424. if (force == false) {
  15425. this._formClustersByZoom();
  15426. }
  15427. else {
  15428. this._forceClustersByZoom();
  15429. }
  15430. },
  15431. /**
  15432. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  15433. *
  15434. * @private
  15435. */
  15436. _formClustersByZoom : function() {
  15437. var dx,dy,length,
  15438. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  15439. // check if any edges are shorter than minLength and start the clustering
  15440. // the clustering favours the node with the larger mass
  15441. for (var edgeId in this.edges) {
  15442. if (this.edges.hasOwnProperty(edgeId)) {
  15443. var edge = this.edges[edgeId];
  15444. if (edge.connected) {
  15445. if (edge.toId != edge.fromId) {
  15446. dx = (edge.to.x - edge.from.x);
  15447. dy = (edge.to.y - edge.from.y);
  15448. length = Math.sqrt(dx * dx + dy * dy);
  15449. if (length < minLength) {
  15450. // first check which node is larger
  15451. var parentNode = edge.from;
  15452. var childNode = edge.to;
  15453. if (edge.to.mass > edge.from.mass) {
  15454. parentNode = edge.to;
  15455. childNode = edge.from;
  15456. }
  15457. if (childNode.dynamicEdgesLength == 1) {
  15458. this._addToCluster(parentNode,childNode,false);
  15459. }
  15460. else if (parentNode.dynamicEdgesLength == 1) {
  15461. this._addToCluster(childNode,parentNode,false);
  15462. }
  15463. }
  15464. }
  15465. }
  15466. }
  15467. }
  15468. },
  15469. /**
  15470. * This function forces the network to cluster all nodes with only one connecting edge to their
  15471. * connected node.
  15472. *
  15473. * @private
  15474. */
  15475. _forceClustersByZoom : function() {
  15476. for (var nodeId in this.nodes) {
  15477. // another node could have absorbed this child.
  15478. if (this.nodes.hasOwnProperty(nodeId)) {
  15479. var childNode = this.nodes[nodeId];
  15480. // the edges can be swallowed by another decrease
  15481. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  15482. var edge = childNode.dynamicEdges[0];
  15483. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  15484. // group to the largest node
  15485. if (childNode.id != parentNode.id) {
  15486. if (parentNode.mass > childNode.mass) {
  15487. this._addToCluster(parentNode,childNode,true);
  15488. }
  15489. else {
  15490. this._addToCluster(childNode,parentNode,true);
  15491. }
  15492. }
  15493. }
  15494. }
  15495. }
  15496. },
  15497. /**
  15498. * To keep the nodes of roughly equal size we normalize the cluster levels.
  15499. * This function clusters a node to its smallest connected neighbour.
  15500. *
  15501. * @param node
  15502. * @private
  15503. */
  15504. _clusterToSmallestNeighbour : function(node) {
  15505. var smallestNeighbour = -1;
  15506. var smallestNeighbourNode = null;
  15507. for (var i = 0; i < node.dynamicEdges.length; i++) {
  15508. if (node.dynamicEdges[i] !== undefined) {
  15509. var neighbour = null;
  15510. if (node.dynamicEdges[i].fromId != node.id) {
  15511. neighbour = node.dynamicEdges[i].from;
  15512. }
  15513. else if (node.dynamicEdges[i].toId != node.id) {
  15514. neighbour = node.dynamicEdges[i].to;
  15515. }
  15516. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  15517. smallestNeighbour = neighbour.clusterSessions.length;
  15518. smallestNeighbourNode = neighbour;
  15519. }
  15520. }
  15521. }
  15522. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  15523. this._addToCluster(neighbour, node, true);
  15524. }
  15525. },
  15526. /**
  15527. * This function forms clusters from hubs, it loops over all nodes
  15528. *
  15529. * @param {Boolean} force | Disregard zoom level
  15530. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  15531. * @private
  15532. */
  15533. _formClustersByHub : function(force, onlyEqual) {
  15534. // we loop over all nodes in the list
  15535. for (var nodeId in this.nodes) {
  15536. // we check if it is still available since it can be used by the clustering in this loop
  15537. if (this.nodes.hasOwnProperty(nodeId)) {
  15538. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  15539. }
  15540. }
  15541. },
  15542. /**
  15543. * This function forms a cluster from a specific preselected hub node
  15544. *
  15545. * @param {Node} hubNode | the node we will cluster as a hub
  15546. * @param {Boolean} force | Disregard zoom level
  15547. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  15548. * @param {Number} [absorptionSizeOffset] |
  15549. * @private
  15550. */
  15551. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  15552. if (absorptionSizeOffset === undefined) {
  15553. absorptionSizeOffset = 0;
  15554. }
  15555. // we decide if the node is a hub
  15556. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  15557. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  15558. // initialize variables
  15559. var dx,dy,length;
  15560. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  15561. var allowCluster = false;
  15562. // we create a list of edges because the dynamicEdges change over the course of this loop
  15563. var edgesIdarray = [];
  15564. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  15565. for (var j = 0; j < amountOfInitialEdges; j++) {
  15566. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  15567. }
  15568. // if the hub clustering is not forces, we check if one of the edges connected
  15569. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  15570. if (force == false) {
  15571. allowCluster = false;
  15572. for (j = 0; j < amountOfInitialEdges; j++) {
  15573. var edge = this.edges[edgesIdarray[j]];
  15574. if (edge !== undefined) {
  15575. if (edge.connected) {
  15576. if (edge.toId != edge.fromId) {
  15577. dx = (edge.to.x - edge.from.x);
  15578. dy = (edge.to.y - edge.from.y);
  15579. length = Math.sqrt(dx * dx + dy * dy);
  15580. if (length < minLength) {
  15581. allowCluster = true;
  15582. break;
  15583. }
  15584. }
  15585. }
  15586. }
  15587. }
  15588. }
  15589. // start the clustering if allowed
  15590. if ((!force && allowCluster) || force) {
  15591. // we loop over all edges INITIALLY connected to this hub
  15592. for (j = 0; j < amountOfInitialEdges; j++) {
  15593. edge = this.edges[edgesIdarray[j]];
  15594. // the edge can be clustered by this function in a previous loop
  15595. if (edge !== undefined) {
  15596. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  15597. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  15598. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  15599. (childNode.id != hubNode.id)) {
  15600. this._addToCluster(hubNode,childNode,force);
  15601. }
  15602. }
  15603. }
  15604. }
  15605. }
  15606. },
  15607. /**
  15608. * This function adds the child node to the parent node, creating a cluster if it is not already.
  15609. *
  15610. * @param {Node} parentNode | this is the node that will house the child node
  15611. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  15612. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  15613. * @private
  15614. */
  15615. _addToCluster : function(parentNode, childNode, force) {
  15616. // join child node in the parent node
  15617. parentNode.containedNodes[childNode.id] = childNode;
  15618. // manage all the edges connected to the child and parent nodes
  15619. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  15620. var edge = childNode.dynamicEdges[i];
  15621. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  15622. this._addToContainedEdges(parentNode,childNode,edge);
  15623. }
  15624. else {
  15625. this._connectEdgeToCluster(parentNode,childNode,edge);
  15626. }
  15627. }
  15628. // a contained node has no dynamic edges.
  15629. childNode.dynamicEdges = [];
  15630. // remove circular edges from clusters
  15631. this._containCircularEdgesFromNode(parentNode,childNode);
  15632. // remove the childNode from the global nodes object
  15633. delete this.nodes[childNode.id];
  15634. // update the properties of the child and parent
  15635. var massBefore = parentNode.mass;
  15636. childNode.clusterSession = this.clusterSession;
  15637. parentNode.mass += childNode.mass;
  15638. parentNode.clusterSize += childNode.clusterSize;
  15639. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  15640. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  15641. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  15642. parentNode.clusterSessions.push(this.clusterSession);
  15643. }
  15644. // forced clusters only open from screen size and double tap
  15645. if (force == true) {
  15646. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  15647. parentNode.formationScale = 0;
  15648. }
  15649. else {
  15650. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  15651. }
  15652. // recalculate the size of the node on the next time the node is rendered
  15653. parentNode.clearSizeCache();
  15654. // set the pop-out scale for the childnode
  15655. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  15656. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  15657. childNode.clearVelocity();
  15658. // the mass has altered, preservation of energy dictates the velocity to be updated
  15659. parentNode.updateVelocity(massBefore);
  15660. // restart the simulation to reorganise all nodes
  15661. this.moving = true;
  15662. },
  15663. /**
  15664. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  15665. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  15666. * It has to be called if a level is collapsed. It is called by _formClusters().
  15667. * @private
  15668. */
  15669. _updateDynamicEdges : function() {
  15670. for (var i = 0; i < this.nodeIndices.length; i++) {
  15671. var node = this.nodes[this.nodeIndices[i]];
  15672. node.dynamicEdgesLength = node.dynamicEdges.length;
  15673. // this corrects for multiple edges pointing at the same other node
  15674. var correction = 0;
  15675. if (node.dynamicEdgesLength > 1) {
  15676. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  15677. var edgeToId = node.dynamicEdges[j].toId;
  15678. var edgeFromId = node.dynamicEdges[j].fromId;
  15679. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  15680. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  15681. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  15682. correction += 1;
  15683. }
  15684. }
  15685. }
  15686. }
  15687. node.dynamicEdgesLength -= correction;
  15688. }
  15689. },
  15690. /**
  15691. * This adds an edge from the childNode to the contained edges of the parent node
  15692. *
  15693. * @param parentNode | Node object
  15694. * @param childNode | Node object
  15695. * @param edge | Edge object
  15696. * @private
  15697. */
  15698. _addToContainedEdges : function(parentNode, childNode, edge) {
  15699. // create an array object if it does not yet exist for this childNode
  15700. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  15701. parentNode.containedEdges[childNode.id] = []
  15702. }
  15703. // add this edge to the list
  15704. parentNode.containedEdges[childNode.id].push(edge);
  15705. // remove the edge from the global edges object
  15706. delete this.edges[edge.id];
  15707. // remove the edge from the parent object
  15708. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  15709. if (parentNode.dynamicEdges[i].id == edge.id) {
  15710. parentNode.dynamicEdges.splice(i,1);
  15711. break;
  15712. }
  15713. }
  15714. },
  15715. /**
  15716. * This function connects an edge that was connected to a child node to the parent node.
  15717. * It keeps track of which nodes it has been connected to with the originalId array.
  15718. *
  15719. * @param {Node} parentNode | Node object
  15720. * @param {Node} childNode | Node object
  15721. * @param {Edge} edge | Edge object
  15722. * @private
  15723. */
  15724. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  15725. // handle circular edges
  15726. if (edge.toId == edge.fromId) {
  15727. this._addToContainedEdges(parentNode, childNode, edge);
  15728. }
  15729. else {
  15730. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  15731. edge.originalToId.push(childNode.id);
  15732. edge.to = parentNode;
  15733. edge.toId = parentNode.id;
  15734. }
  15735. else { // edge connected to other node with the "from" side
  15736. edge.originalFromId.push(childNode.id);
  15737. edge.from = parentNode;
  15738. edge.fromId = parentNode.id;
  15739. }
  15740. this._addToReroutedEdges(parentNode,childNode,edge);
  15741. }
  15742. },
  15743. /**
  15744. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  15745. * these edges inside of the cluster.
  15746. *
  15747. * @param parentNode
  15748. * @param childNode
  15749. * @private
  15750. */
  15751. _containCircularEdgesFromNode : function(parentNode, childNode) {
  15752. // manage all the edges connected to the child and parent nodes
  15753. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  15754. var edge = parentNode.dynamicEdges[i];
  15755. // handle circular edges
  15756. if (edge.toId == edge.fromId) {
  15757. this._addToContainedEdges(parentNode, childNode, edge);
  15758. }
  15759. }
  15760. },
  15761. /**
  15762. * This adds an edge from the childNode to the rerouted edges of the parent node
  15763. *
  15764. * @param parentNode | Node object
  15765. * @param childNode | Node object
  15766. * @param edge | Edge object
  15767. * @private
  15768. */
  15769. _addToReroutedEdges : function(parentNode, childNode, edge) {
  15770. // create an array object if it does not yet exist for this childNode
  15771. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  15772. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  15773. parentNode.reroutedEdges[childNode.id] = [];
  15774. }
  15775. parentNode.reroutedEdges[childNode.id].push(edge);
  15776. // this edge becomes part of the dynamicEdges of the cluster node
  15777. parentNode.dynamicEdges.push(edge);
  15778. },
  15779. /**
  15780. * This function connects an edge that was connected to a cluster node back to the child node.
  15781. *
  15782. * @param parentNode | Node object
  15783. * @param childNode | Node object
  15784. * @private
  15785. */
  15786. _connectEdgeBackToChild : function(parentNode, childNode) {
  15787. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  15788. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  15789. var edge = parentNode.reroutedEdges[childNode.id][i];
  15790. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  15791. edge.originalFromId.pop();
  15792. edge.fromId = childNode.id;
  15793. edge.from = childNode;
  15794. }
  15795. else {
  15796. edge.originalToId.pop();
  15797. edge.toId = childNode.id;
  15798. edge.to = childNode;
  15799. }
  15800. // append this edge to the list of edges connecting to the childnode
  15801. childNode.dynamicEdges.push(edge);
  15802. // remove the edge from the parent object
  15803. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  15804. if (parentNode.dynamicEdges[j].id == edge.id) {
  15805. parentNode.dynamicEdges.splice(j,1);
  15806. break;
  15807. }
  15808. }
  15809. }
  15810. // remove the entry from the rerouted edges
  15811. delete parentNode.reroutedEdges[childNode.id];
  15812. }
  15813. },
  15814. /**
  15815. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  15816. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  15817. * parentNode
  15818. *
  15819. * @param parentNode | Node object
  15820. * @private
  15821. */
  15822. _validateEdges : function(parentNode) {
  15823. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  15824. var edge = parentNode.dynamicEdges[i];
  15825. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  15826. parentNode.dynamicEdges.splice(i,1);
  15827. }
  15828. }
  15829. },
  15830. /**
  15831. * This function released the contained edges back into the global domain and puts them back into the
  15832. * dynamic edges of both parent and child.
  15833. *
  15834. * @param {Node} parentNode |
  15835. * @param {Node} childNode |
  15836. * @private
  15837. */
  15838. _releaseContainedEdges : function(parentNode, childNode) {
  15839. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  15840. var edge = parentNode.containedEdges[childNode.id][i];
  15841. // put the edge back in the global edges object
  15842. this.edges[edge.id] = edge;
  15843. // put the edge back in the dynamic edges of the child and parent
  15844. childNode.dynamicEdges.push(edge);
  15845. parentNode.dynamicEdges.push(edge);
  15846. }
  15847. // remove the entry from the contained edges
  15848. delete parentNode.containedEdges[childNode.id];
  15849. },
  15850. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  15851. /**
  15852. * This updates the node labels for all nodes (for debugging purposes)
  15853. */
  15854. updateLabels : function() {
  15855. var nodeId;
  15856. // update node labels
  15857. for (nodeId in this.nodes) {
  15858. if (this.nodes.hasOwnProperty(nodeId)) {
  15859. var node = this.nodes[nodeId];
  15860. if (node.clusterSize > 1) {
  15861. node.label = "[".concat(String(node.clusterSize),"]");
  15862. }
  15863. }
  15864. }
  15865. // update node labels
  15866. for (nodeId in this.nodes) {
  15867. if (this.nodes.hasOwnProperty(nodeId)) {
  15868. node = this.nodes[nodeId];
  15869. if (node.clusterSize == 1) {
  15870. if (node.originalLabel !== undefined) {
  15871. node.label = node.originalLabel;
  15872. }
  15873. else {
  15874. node.label = String(node.id);
  15875. }
  15876. }
  15877. }
  15878. }
  15879. // /* Debug Override */
  15880. // for (nodeId in this.nodes) {
  15881. // if (this.nodes.hasOwnProperty(nodeId)) {
  15882. // node = this.nodes[nodeId];
  15883. // node.label = String(node.level);
  15884. // }
  15885. // }
  15886. },
  15887. /**
  15888. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  15889. * if the rest of the nodes are already a few cluster levels in.
  15890. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  15891. * clustered enough to the clusterToSmallestNeighbours function.
  15892. */
  15893. normalizeClusterLevels : function() {
  15894. var maxLevel = 0;
  15895. var minLevel = 1e9;
  15896. var clusterLevel = 0;
  15897. var nodeId;
  15898. // we loop over all nodes in the list
  15899. for (nodeId in this.nodes) {
  15900. if (this.nodes.hasOwnProperty(nodeId)) {
  15901. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  15902. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  15903. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  15904. }
  15905. }
  15906. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  15907. var amountOfNodes = this.nodeIndices.length;
  15908. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  15909. // we loop over all nodes in the list
  15910. for (nodeId in this.nodes) {
  15911. if (this.nodes.hasOwnProperty(nodeId)) {
  15912. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  15913. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  15914. }
  15915. }
  15916. }
  15917. this._updateNodeIndexList();
  15918. this._updateDynamicEdges();
  15919. // if a cluster was formed, we increase the clusterSession
  15920. if (this.nodeIndices.length != amountOfNodes) {
  15921. this.clusterSession += 1;
  15922. }
  15923. }
  15924. },
  15925. /**
  15926. * This function determines if the cluster we want to decluster is in the active area
  15927. * this means around the zoom center
  15928. *
  15929. * @param {Node} node
  15930. * @returns {boolean}
  15931. * @private
  15932. */
  15933. _nodeInActiveArea : function(node) {
  15934. return (
  15935. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  15936. &&
  15937. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  15938. )
  15939. },
  15940. /**
  15941. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  15942. * It puts large clusters away from the center and randomizes the order.
  15943. *
  15944. */
  15945. repositionNodes : function() {
  15946. for (var i = 0; i < this.nodeIndices.length; i++) {
  15947. var node = this.nodes[this.nodeIndices[i]];
  15948. if ((node.xFixed == false || node.yFixed == false)) {
  15949. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  15950. var angle = 2 * Math.PI * Math.random();
  15951. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15952. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15953. this._repositionBezierNodes(node);
  15954. }
  15955. }
  15956. },
  15957. /**
  15958. * We determine how many connections denote an important hub.
  15959. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  15960. *
  15961. * @private
  15962. */
  15963. _getHubSize : function() {
  15964. var average = 0;
  15965. var averageSquared = 0;
  15966. var hubCounter = 0;
  15967. var largestHub = 0;
  15968. for (var i = 0; i < this.nodeIndices.length; i++) {
  15969. var node = this.nodes[this.nodeIndices[i]];
  15970. if (node.dynamicEdgesLength > largestHub) {
  15971. largestHub = node.dynamicEdgesLength;
  15972. }
  15973. average += node.dynamicEdgesLength;
  15974. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  15975. hubCounter += 1;
  15976. }
  15977. average = average / hubCounter;
  15978. averageSquared = averageSquared / hubCounter;
  15979. var variance = averageSquared - Math.pow(average,2);
  15980. var standardDeviation = Math.sqrt(variance);
  15981. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  15982. // always have at least one to cluster
  15983. if (this.hubThreshold > largestHub) {
  15984. this.hubThreshold = largestHub;
  15985. }
  15986. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  15987. // console.log("hubThreshold:",this.hubThreshold);
  15988. },
  15989. /**
  15990. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  15991. * with this amount we can cluster specifically on these chains.
  15992. *
  15993. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  15994. * @private
  15995. */
  15996. _reduceAmountOfChains : function(fraction) {
  15997. this.hubThreshold = 2;
  15998. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  15999. for (var nodeId in this.nodes) {
  16000. if (this.nodes.hasOwnProperty(nodeId)) {
  16001. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  16002. if (reduceAmount > 0) {
  16003. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  16004. reduceAmount -= 1;
  16005. }
  16006. }
  16007. }
  16008. }
  16009. },
  16010. /**
  16011. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  16012. * with this amount we can cluster specifically on these chains.
  16013. *
  16014. * @private
  16015. */
  16016. _getChainFraction : function() {
  16017. var chains = 0;
  16018. var total = 0;
  16019. for (var nodeId in this.nodes) {
  16020. if (this.nodes.hasOwnProperty(nodeId)) {
  16021. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  16022. chains += 1;
  16023. }
  16024. total += 1;
  16025. }
  16026. }
  16027. return chains/total;
  16028. }
  16029. };
  16030. var SelectionMixin = {
  16031. /**
  16032. * This function can be called from the _doInAllSectors function
  16033. *
  16034. * @param object
  16035. * @param overlappingNodes
  16036. * @private
  16037. */
  16038. _getNodesOverlappingWith : function(object, overlappingNodes) {
  16039. var nodes = this.nodes;
  16040. for (var nodeId in nodes) {
  16041. if (nodes.hasOwnProperty(nodeId)) {
  16042. if (nodes[nodeId].isOverlappingWith(object)) {
  16043. overlappingNodes.push(nodeId);
  16044. }
  16045. }
  16046. }
  16047. },
  16048. /**
  16049. * retrieve all nodes overlapping with given object
  16050. * @param {Object} object An object with parameters left, top, right, bottom
  16051. * @return {Number[]} An array with id's of the overlapping nodes
  16052. * @private
  16053. */
  16054. _getAllNodesOverlappingWith : function (object) {
  16055. var overlappingNodes = [];
  16056. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  16057. return overlappingNodes;
  16058. },
  16059. /**
  16060. * Return a position object in canvasspace from a single point in screenspace
  16061. *
  16062. * @param pointer
  16063. * @returns {{left: number, top: number, right: number, bottom: number}}
  16064. * @private
  16065. */
  16066. _pointerToPositionObject : function(pointer) {
  16067. var x = this._XconvertDOMtoCanvas(pointer.x);
  16068. var y = this._YconvertDOMtoCanvas(pointer.y);
  16069. return {left: x,
  16070. top: y,
  16071. right: x,
  16072. bottom: y};
  16073. },
  16074. /**
  16075. * Get the top node at the a specific point (like a click)
  16076. *
  16077. * @param {{x: Number, y: Number}} pointer
  16078. * @return {Node | null} node
  16079. * @private
  16080. */
  16081. _getNodeAt : function (pointer) {
  16082. // we first check if this is an navigation controls element
  16083. var positionObject = this._pointerToPositionObject(pointer);
  16084. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  16085. // if there are overlapping nodes, select the last one, this is the
  16086. // one which is drawn on top of the others
  16087. if (overlappingNodes.length > 0) {
  16088. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  16089. }
  16090. else {
  16091. return null;
  16092. }
  16093. },
  16094. /**
  16095. * retrieve all edges overlapping with given object, selector is around center
  16096. * @param {Object} object An object with parameters left, top, right, bottom
  16097. * @return {Number[]} An array with id's of the overlapping nodes
  16098. * @private
  16099. */
  16100. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  16101. var edges = this.edges;
  16102. for (var edgeId in edges) {
  16103. if (edges.hasOwnProperty(edgeId)) {
  16104. if (edges[edgeId].isOverlappingWith(object)) {
  16105. overlappingEdges.push(edgeId);
  16106. }
  16107. }
  16108. }
  16109. },
  16110. /**
  16111. * retrieve all nodes overlapping with given object
  16112. * @param {Object} object An object with parameters left, top, right, bottom
  16113. * @return {Number[]} An array with id's of the overlapping nodes
  16114. * @private
  16115. */
  16116. _getAllEdgesOverlappingWith : function (object) {
  16117. var overlappingEdges = [];
  16118. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  16119. return overlappingEdges;
  16120. },
  16121. /**
  16122. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  16123. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  16124. *
  16125. * @param pointer
  16126. * @returns {null}
  16127. * @private
  16128. */
  16129. _getEdgeAt : function(pointer) {
  16130. var positionObject = this._pointerToPositionObject(pointer);
  16131. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  16132. if (overlappingEdges.length > 0) {
  16133. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  16134. }
  16135. else {
  16136. return null;
  16137. }
  16138. },
  16139. /**
  16140. * Add object to the selection array.
  16141. *
  16142. * @param obj
  16143. * @private
  16144. */
  16145. _addToSelection : function(obj) {
  16146. if (obj instanceof Node) {
  16147. this.selectionObj.nodes[obj.id] = obj;
  16148. }
  16149. else {
  16150. this.selectionObj.edges[obj.id] = obj;
  16151. }
  16152. },
  16153. /**
  16154. * Add object to the selection array.
  16155. *
  16156. * @param obj
  16157. * @private
  16158. */
  16159. _addToHover : function(obj) {
  16160. if (obj instanceof Node) {
  16161. this.hoverObj.nodes[obj.id] = obj;
  16162. }
  16163. else {
  16164. this.hoverObj.edges[obj.id] = obj;
  16165. }
  16166. },
  16167. /**
  16168. * Remove a single option from selection.
  16169. *
  16170. * @param {Object} obj
  16171. * @private
  16172. */
  16173. _removeFromSelection : function(obj) {
  16174. if (obj instanceof Node) {
  16175. delete this.selectionObj.nodes[obj.id];
  16176. }
  16177. else {
  16178. delete this.selectionObj.edges[obj.id];
  16179. }
  16180. },
  16181. /**
  16182. * Unselect all. The selectionObj is useful for this.
  16183. *
  16184. * @param {Boolean} [doNotTrigger] | ignore trigger
  16185. * @private
  16186. */
  16187. _unselectAll : function(doNotTrigger) {
  16188. if (doNotTrigger === undefined) {
  16189. doNotTrigger = false;
  16190. }
  16191. for(var nodeId in this.selectionObj.nodes) {
  16192. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16193. this.selectionObj.nodes[nodeId].unselect();
  16194. }
  16195. }
  16196. for(var edgeId in this.selectionObj.edges) {
  16197. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16198. this.selectionObj.edges[edgeId].unselect();
  16199. }
  16200. }
  16201. this.selectionObj = {nodes:{},edges:{}};
  16202. if (doNotTrigger == false) {
  16203. this.emit('select', this.getSelection());
  16204. }
  16205. },
  16206. /**
  16207. * Unselect all clusters. The selectionObj is useful for this.
  16208. *
  16209. * @param {Boolean} [doNotTrigger] | ignore trigger
  16210. * @private
  16211. */
  16212. _unselectClusters : function(doNotTrigger) {
  16213. if (doNotTrigger === undefined) {
  16214. doNotTrigger = false;
  16215. }
  16216. for (var nodeId in this.selectionObj.nodes) {
  16217. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16218. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  16219. this.selectionObj.nodes[nodeId].unselect();
  16220. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  16221. }
  16222. }
  16223. }
  16224. if (doNotTrigger == false) {
  16225. this.emit('select', this.getSelection());
  16226. }
  16227. },
  16228. /**
  16229. * return the number of selected nodes
  16230. *
  16231. * @returns {number}
  16232. * @private
  16233. */
  16234. _getSelectedNodeCount : function() {
  16235. var count = 0;
  16236. for (var nodeId in this.selectionObj.nodes) {
  16237. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16238. count += 1;
  16239. }
  16240. }
  16241. return count;
  16242. },
  16243. /**
  16244. * return the selected node
  16245. *
  16246. * @returns {number}
  16247. * @private
  16248. */
  16249. _getSelectedNode : function() {
  16250. for (var nodeId in this.selectionObj.nodes) {
  16251. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16252. return this.selectionObj.nodes[nodeId];
  16253. }
  16254. }
  16255. return null;
  16256. },
  16257. /**
  16258. * return the selected edge
  16259. *
  16260. * @returns {number}
  16261. * @private
  16262. */
  16263. _getSelectedEdge : function() {
  16264. for (var edgeId in this.selectionObj.edges) {
  16265. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16266. return this.selectionObj.edges[edgeId];
  16267. }
  16268. }
  16269. return null;
  16270. },
  16271. /**
  16272. * return the number of selected edges
  16273. *
  16274. * @returns {number}
  16275. * @private
  16276. */
  16277. _getSelectedEdgeCount : function() {
  16278. var count = 0;
  16279. for (var edgeId in this.selectionObj.edges) {
  16280. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16281. count += 1;
  16282. }
  16283. }
  16284. return count;
  16285. },
  16286. /**
  16287. * return the number of selected objects.
  16288. *
  16289. * @returns {number}
  16290. * @private
  16291. */
  16292. _getSelectedObjectCount : function() {
  16293. var count = 0;
  16294. for(var nodeId in this.selectionObj.nodes) {
  16295. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16296. count += 1;
  16297. }
  16298. }
  16299. for(var edgeId in this.selectionObj.edges) {
  16300. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16301. count += 1;
  16302. }
  16303. }
  16304. return count;
  16305. },
  16306. /**
  16307. * Check if anything is selected
  16308. *
  16309. * @returns {boolean}
  16310. * @private
  16311. */
  16312. _selectionIsEmpty : function() {
  16313. for(var nodeId in this.selectionObj.nodes) {
  16314. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16315. return false;
  16316. }
  16317. }
  16318. for(var edgeId in this.selectionObj.edges) {
  16319. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16320. return false;
  16321. }
  16322. }
  16323. return true;
  16324. },
  16325. /**
  16326. * check if one of the selected nodes is a cluster.
  16327. *
  16328. * @returns {boolean}
  16329. * @private
  16330. */
  16331. _clusterInSelection : function() {
  16332. for(var nodeId in this.selectionObj.nodes) {
  16333. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16334. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  16335. return true;
  16336. }
  16337. }
  16338. }
  16339. return false;
  16340. },
  16341. /**
  16342. * select the edges connected to the node that is being selected
  16343. *
  16344. * @param {Node} node
  16345. * @private
  16346. */
  16347. _selectConnectedEdges : function(node) {
  16348. for (var i = 0; i < node.dynamicEdges.length; i++) {
  16349. var edge = node.dynamicEdges[i];
  16350. edge.select();
  16351. this._addToSelection(edge);
  16352. }
  16353. },
  16354. /**
  16355. * select the edges connected to the node that is being selected
  16356. *
  16357. * @param {Node} node
  16358. * @private
  16359. */
  16360. _hoverConnectedEdges : function(node) {
  16361. for (var i = 0; i < node.dynamicEdges.length; i++) {
  16362. var edge = node.dynamicEdges[i];
  16363. edge.hover = true;
  16364. this._addToHover(edge);
  16365. }
  16366. },
  16367. /**
  16368. * unselect the edges connected to the node that is being selected
  16369. *
  16370. * @param {Node} node
  16371. * @private
  16372. */
  16373. _unselectConnectedEdges : function(node) {
  16374. for (var i = 0; i < node.dynamicEdges.length; i++) {
  16375. var edge = node.dynamicEdges[i];
  16376. edge.unselect();
  16377. this._removeFromSelection(edge);
  16378. }
  16379. },
  16380. /**
  16381. * This is called when someone clicks on a node. either select or deselect it.
  16382. * If there is an existing selection and we don't want to append to it, clear the existing selection
  16383. *
  16384. * @param {Node || Edge} object
  16385. * @param {Boolean} append
  16386. * @param {Boolean} [doNotTrigger] | ignore trigger
  16387. * @private
  16388. */
  16389. _selectObject : function(object, append, doNotTrigger, highlightEdges) {
  16390. if (doNotTrigger === undefined) {
  16391. doNotTrigger = false;
  16392. }
  16393. if (highlightEdges === undefined) {
  16394. highlightEdges = true;
  16395. }
  16396. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  16397. this._unselectAll(true);
  16398. }
  16399. if (object.selected == false) {
  16400. object.select();
  16401. this._addToSelection(object);
  16402. if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
  16403. this._selectConnectedEdges(object);
  16404. }
  16405. }
  16406. else {
  16407. object.unselect();
  16408. this._removeFromSelection(object);
  16409. }
  16410. if (doNotTrigger == false) {
  16411. this.emit('select', this.getSelection());
  16412. }
  16413. },
  16414. /**
  16415. * This is called when someone clicks on a node. either select or deselect it.
  16416. * If there is an existing selection and we don't want to append to it, clear the existing selection
  16417. *
  16418. * @param {Node || Edge} object
  16419. * @private
  16420. */
  16421. _blurObject : function(object) {
  16422. if (object.hover == true) {
  16423. object.hover = false;
  16424. this.emit("blurNode",{node:object.id});
  16425. }
  16426. },
  16427. /**
  16428. * This is called when someone clicks on a node. either select or deselect it.
  16429. * If there is an existing selection and we don't want to append to it, clear the existing selection
  16430. *
  16431. * @param {Node || Edge} object
  16432. * @private
  16433. */
  16434. _hoverObject : function(object) {
  16435. if (object.hover == false) {
  16436. object.hover = true;
  16437. this._addToHover(object);
  16438. if (object instanceof Node) {
  16439. this.emit("hoverNode",{node:object.id});
  16440. }
  16441. }
  16442. if (object instanceof Node) {
  16443. this._hoverConnectedEdges(object);
  16444. }
  16445. },
  16446. /**
  16447. * handles the selection part of the touch, only for navigation controls elements;
  16448. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  16449. * This is the most responsive solution
  16450. *
  16451. * @param {Object} pointer
  16452. * @private
  16453. */
  16454. _handleTouch : function(pointer) {
  16455. },
  16456. /**
  16457. * handles the selection part of the tap;
  16458. *
  16459. * @param {Object} pointer
  16460. * @private
  16461. */
  16462. _handleTap : function(pointer) {
  16463. var node = this._getNodeAt(pointer);
  16464. if (node != null) {
  16465. this._selectObject(node,false);
  16466. }
  16467. else {
  16468. var edge = this._getEdgeAt(pointer);
  16469. if (edge != null) {
  16470. this._selectObject(edge,false);
  16471. }
  16472. else {
  16473. this._unselectAll();
  16474. }
  16475. }
  16476. this.emit("click", this.getSelection());
  16477. this._redraw();
  16478. },
  16479. /**
  16480. * handles the selection part of the double tap and opens a cluster if needed
  16481. *
  16482. * @param {Object} pointer
  16483. * @private
  16484. */
  16485. _handleDoubleTap : function(pointer) {
  16486. var node = this._getNodeAt(pointer);
  16487. if (node != null && node !== undefined) {
  16488. // we reset the areaCenter here so the opening of the node will occur
  16489. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  16490. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  16491. this.openCluster(node);
  16492. }
  16493. this.emit("doubleClick", this.getSelection());
  16494. },
  16495. /**
  16496. * Handle the onHold selection part
  16497. *
  16498. * @param pointer
  16499. * @private
  16500. */
  16501. _handleOnHold : function(pointer) {
  16502. var node = this._getNodeAt(pointer);
  16503. if (node != null) {
  16504. this._selectObject(node,true);
  16505. }
  16506. else {
  16507. var edge = this._getEdgeAt(pointer);
  16508. if (edge != null) {
  16509. this._selectObject(edge,true);
  16510. }
  16511. }
  16512. this._redraw();
  16513. },
  16514. /**
  16515. * handle the onRelease event. These functions are here for the navigation controls module.
  16516. *
  16517. * @private
  16518. */
  16519. _handleOnRelease : function(pointer) {
  16520. },
  16521. /**
  16522. *
  16523. * retrieve the currently selected objects
  16524. * @return {Number[] | String[]} selection An array with the ids of the
  16525. * selected nodes.
  16526. */
  16527. getSelection : function() {
  16528. var nodeIds = this.getSelectedNodes();
  16529. var edgeIds = this.getSelectedEdges();
  16530. return {nodes:nodeIds, edges:edgeIds};
  16531. },
  16532. /**
  16533. *
  16534. * retrieve the currently selected nodes
  16535. * @return {String} selection An array with the ids of the
  16536. * selected nodes.
  16537. */
  16538. getSelectedNodes : function() {
  16539. var idArray = [];
  16540. for(var nodeId in this.selectionObj.nodes) {
  16541. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16542. idArray.push(nodeId);
  16543. }
  16544. }
  16545. return idArray
  16546. },
  16547. /**
  16548. *
  16549. * retrieve the currently selected edges
  16550. * @return {Array} selection An array with the ids of the
  16551. * selected nodes.
  16552. */
  16553. getSelectedEdges : function() {
  16554. var idArray = [];
  16555. for(var edgeId in this.selectionObj.edges) {
  16556. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16557. idArray.push(edgeId);
  16558. }
  16559. }
  16560. return idArray;
  16561. },
  16562. /**
  16563. * select zero or more nodes
  16564. * @param {Number[] | String[]} selection An array with the ids of the
  16565. * selected nodes.
  16566. */
  16567. setSelection : function(selection) {
  16568. var i, iMax, id;
  16569. if (!selection || (selection.length == undefined))
  16570. throw 'Selection must be an array with ids';
  16571. // first unselect any selected node
  16572. this._unselectAll(true);
  16573. for (i = 0, iMax = selection.length; i < iMax; i++) {
  16574. id = selection[i];
  16575. var node = this.nodes[id];
  16576. if (!node) {
  16577. throw new RangeError('Node with id "' + id + '" not found');
  16578. }
  16579. this._selectObject(node,true,true);
  16580. }
  16581. console.log("setSelection is deprecated. Please use selectNodes instead.")
  16582. this.redraw();
  16583. },
  16584. /**
  16585. * select zero or more nodes with the option to highlight edges
  16586. * @param {Number[] | String[]} selection An array with the ids of the
  16587. * selected nodes.
  16588. * @param {boolean} [highlightEdges]
  16589. */
  16590. selectNodes : function(selection, highlightEdges) {
  16591. var i, iMax, id;
  16592. if (!selection || (selection.length == undefined))
  16593. throw 'Selection must be an array with ids';
  16594. // first unselect any selected node
  16595. this._unselectAll(true);
  16596. for (i = 0, iMax = selection.length; i < iMax; i++) {
  16597. id = selection[i];
  16598. var node = this.nodes[id];
  16599. if (!node) {
  16600. throw new RangeError('Node with id "' + id + '" not found');
  16601. }
  16602. this._selectObject(node,true,true,highlightEdges);
  16603. }
  16604. this.redraw();
  16605. },
  16606. /**
  16607. * select zero or more edges
  16608. * @param {Number[] | String[]} selection An array with the ids of the
  16609. * selected nodes.
  16610. */
  16611. selectEdges : function(selection) {
  16612. var i, iMax, id;
  16613. if (!selection || (selection.length == undefined))
  16614. throw 'Selection must be an array with ids';
  16615. // first unselect any selected node
  16616. this._unselectAll(true);
  16617. for (i = 0, iMax = selection.length; i < iMax; i++) {
  16618. id = selection[i];
  16619. var edge = this.edges[id];
  16620. if (!edge) {
  16621. throw new RangeError('Edge with id "' + id + '" not found');
  16622. }
  16623. this._selectObject(edge,true,true,highlightEdges);
  16624. }
  16625. this.redraw();
  16626. },
  16627. /**
  16628. * Validate the selection: remove ids of nodes which no longer exist
  16629. * @private
  16630. */
  16631. _updateSelection : function () {
  16632. for(var nodeId in this.selectionObj.nodes) {
  16633. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16634. if (!this.nodes.hasOwnProperty(nodeId)) {
  16635. delete this.selectionObj.nodes[nodeId];
  16636. }
  16637. }
  16638. }
  16639. for(var edgeId in this.selectionObj.edges) {
  16640. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16641. if (!this.edges.hasOwnProperty(edgeId)) {
  16642. delete this.selectionObj.edges[edgeId];
  16643. }
  16644. }
  16645. }
  16646. }
  16647. };
  16648. /**
  16649. * Created by Alex on 1/22/14.
  16650. */
  16651. var NavigationMixin = {
  16652. _cleanNavigation : function() {
  16653. // clean up previosu navigation items
  16654. var wrapper = document.getElementById('network-navigation_wrapper');
  16655. if (wrapper != null) {
  16656. this.containerElement.removeChild(wrapper);
  16657. }
  16658. document.onmouseup = null;
  16659. },
  16660. /**
  16661. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  16662. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  16663. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  16664. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  16665. *
  16666. * @private
  16667. */
  16668. _loadNavigationElements : function() {
  16669. this._cleanNavigation();
  16670. this.navigationDivs = {};
  16671. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  16672. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  16673. this.navigationDivs['wrapper'] = document.createElement('div');
  16674. this.navigationDivs['wrapper'].id = "network-navigation_wrapper";
  16675. this.navigationDivs['wrapper'].style.position = "absolute";
  16676. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  16677. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  16678. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  16679. for (var i = 0; i < navigationDivs.length; i++) {
  16680. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  16681. this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
  16682. this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
  16683. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  16684. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  16685. }
  16686. document.onmouseup = this._stopMovement.bind(this);
  16687. },
  16688. /**
  16689. * this stops all movement induced by the navigation buttons
  16690. *
  16691. * @private
  16692. */
  16693. _stopMovement : function() {
  16694. this._xStopMoving();
  16695. this._yStopMoving();
  16696. this._stopZoom();
  16697. },
  16698. /**
  16699. * stops the actions performed by page up and down etc.
  16700. *
  16701. * @param event
  16702. * @private
  16703. */
  16704. _preventDefault : function(event) {
  16705. if (event !== undefined) {
  16706. if (event.preventDefault) {
  16707. event.preventDefault();
  16708. } else {
  16709. event.returnValue = false;
  16710. }
  16711. }
  16712. },
  16713. /**
  16714. * move the screen up
  16715. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  16716. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  16717. * To avoid this behaviour, we do the translation in the start loop.
  16718. *
  16719. * @private
  16720. */
  16721. _moveUp : function(event) {
  16722. this.yIncrement = this.constants.keyboard.speed.y;
  16723. this.start(); // if there is no node movement, the calculation wont be done
  16724. this._preventDefault(event);
  16725. if (this.navigationDivs) {
  16726. this.navigationDivs['up'].className += " active";
  16727. }
  16728. },
  16729. /**
  16730. * move the screen down
  16731. * @private
  16732. */
  16733. _moveDown : function(event) {
  16734. this.yIncrement = -this.constants.keyboard.speed.y;
  16735. this.start(); // if there is no node movement, the calculation wont be done
  16736. this._preventDefault(event);
  16737. if (this.navigationDivs) {
  16738. this.navigationDivs['down'].className += " active";
  16739. }
  16740. },
  16741. /**
  16742. * move the screen left
  16743. * @private
  16744. */
  16745. _moveLeft : function(event) {
  16746. this.xIncrement = this.constants.keyboard.speed.x;
  16747. this.start(); // if there is no node movement, the calculation wont be done
  16748. this._preventDefault(event);
  16749. if (this.navigationDivs) {
  16750. this.navigationDivs['left'].className += " active";
  16751. }
  16752. },
  16753. /**
  16754. * move the screen right
  16755. * @private
  16756. */
  16757. _moveRight : function(event) {
  16758. this.xIncrement = -this.constants.keyboard.speed.y;
  16759. this.start(); // if there is no node movement, the calculation wont be done
  16760. this._preventDefault(event);
  16761. if (this.navigationDivs) {
  16762. this.navigationDivs['right'].className += " active";
  16763. }
  16764. },
  16765. /**
  16766. * Zoom in, using the same method as the movement.
  16767. * @private
  16768. */
  16769. _zoomIn : function(event) {
  16770. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  16771. this.start(); // if there is no node movement, the calculation wont be done
  16772. this._preventDefault(event);
  16773. if (this.navigationDivs) {
  16774. this.navigationDivs['zoomIn'].className += " active";
  16775. }
  16776. },
  16777. /**
  16778. * Zoom out
  16779. * @private
  16780. */
  16781. _zoomOut : function() {
  16782. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  16783. this.start(); // if there is no node movement, the calculation wont be done
  16784. this._preventDefault(event);
  16785. if (this.navigationDivs) {
  16786. this.navigationDivs['zoomOut'].className += " active";
  16787. }
  16788. },
  16789. /**
  16790. * Stop zooming and unhighlight the zoom controls
  16791. * @private
  16792. */
  16793. _stopZoom : function() {
  16794. this.zoomIncrement = 0;
  16795. if (this.navigationDivs) {
  16796. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  16797. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  16798. }
  16799. },
  16800. /**
  16801. * Stop moving in the Y direction and unHighlight the up and down
  16802. * @private
  16803. */
  16804. _yStopMoving : function() {
  16805. this.yIncrement = 0;
  16806. if (this.navigationDivs) {
  16807. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  16808. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  16809. }
  16810. },
  16811. /**
  16812. * Stop moving in the X direction and unHighlight left and right.
  16813. * @private
  16814. */
  16815. _xStopMoving : function() {
  16816. this.xIncrement = 0;
  16817. if (this.navigationDivs) {
  16818. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  16819. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  16820. }
  16821. }
  16822. };
  16823. /**
  16824. * Created by Alex on 2/10/14.
  16825. */
  16826. var networkMixinLoaders = {
  16827. /**
  16828. * Load a mixin into the network object
  16829. *
  16830. * @param {Object} sourceVariable | this object has to contain functions.
  16831. * @private
  16832. */
  16833. _loadMixin: function (sourceVariable) {
  16834. for (var mixinFunction in sourceVariable) {
  16835. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  16836. Network.prototype[mixinFunction] = sourceVariable[mixinFunction];
  16837. }
  16838. }
  16839. },
  16840. /**
  16841. * removes a mixin from the network object.
  16842. *
  16843. * @param {Object} sourceVariable | this object has to contain functions.
  16844. * @private
  16845. */
  16846. _clearMixin: function (sourceVariable) {
  16847. for (var mixinFunction in sourceVariable) {
  16848. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  16849. Network.prototype[mixinFunction] = undefined;
  16850. }
  16851. }
  16852. },
  16853. /**
  16854. * Mixin the physics system and initialize the parameters required.
  16855. *
  16856. * @private
  16857. */
  16858. _loadPhysicsSystem: function () {
  16859. this._loadMixin(physicsMixin);
  16860. this._loadSelectedForceSolver();
  16861. if (this.constants.configurePhysics == true) {
  16862. this._loadPhysicsConfiguration();
  16863. }
  16864. },
  16865. /**
  16866. * Mixin the cluster system and initialize the parameters required.
  16867. *
  16868. * @private
  16869. */
  16870. _loadClusterSystem: function () {
  16871. this.clusterSession = 0;
  16872. this.hubThreshold = 5;
  16873. this._loadMixin(ClusterMixin);
  16874. },
  16875. /**
  16876. * Mixin the sector system and initialize the parameters required
  16877. *
  16878. * @private
  16879. */
  16880. _loadSectorSystem: function () {
  16881. this.sectors = {};
  16882. this.activeSector = ["default"];
  16883. this.sectors["active"] = {};
  16884. this.sectors["active"]["default"] = {"nodes": {},
  16885. "edges": {},
  16886. "nodeIndices": [],
  16887. "formationScale": 1.0,
  16888. "drawingNode": undefined };
  16889. this.sectors["frozen"] = {};
  16890. this.sectors["support"] = {"nodes": {},
  16891. "edges": {},
  16892. "nodeIndices": [],
  16893. "formationScale": 1.0,
  16894. "drawingNode": undefined };
  16895. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  16896. this._loadMixin(SectorMixin);
  16897. },
  16898. /**
  16899. * Mixin the selection system and initialize the parameters required
  16900. *
  16901. * @private
  16902. */
  16903. _loadSelectionSystem: function () {
  16904. this.selectionObj = {nodes: {}, edges: {}};
  16905. this._loadMixin(SelectionMixin);
  16906. },
  16907. /**
  16908. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  16909. *
  16910. * @private
  16911. */
  16912. _loadManipulationSystem: function () {
  16913. // reset global variables -- these are used by the selection of nodes and edges.
  16914. this.blockConnectingEdgeSelection = false;
  16915. this.forceAppendSelection = false;
  16916. if (this.constants.dataManipulation.enabled == true) {
  16917. // load the manipulator HTML elements. All styling done in css.
  16918. if (this.manipulationDiv === undefined) {
  16919. this.manipulationDiv = document.createElement('div');
  16920. this.manipulationDiv.className = 'network-manipulationDiv';
  16921. this.manipulationDiv.id = 'network-manipulationDiv';
  16922. if (this.editMode == true) {
  16923. this.manipulationDiv.style.display = "block";
  16924. }
  16925. else {
  16926. this.manipulationDiv.style.display = "none";
  16927. }
  16928. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  16929. }
  16930. if (this.editModeDiv === undefined) {
  16931. this.editModeDiv = document.createElement('div');
  16932. this.editModeDiv.className = 'network-manipulation-editMode';
  16933. this.editModeDiv.id = 'network-manipulation-editMode';
  16934. if (this.editMode == true) {
  16935. this.editModeDiv.style.display = "none";
  16936. }
  16937. else {
  16938. this.editModeDiv.style.display = "block";
  16939. }
  16940. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  16941. }
  16942. if (this.closeDiv === undefined) {
  16943. this.closeDiv = document.createElement('div');
  16944. this.closeDiv.className = 'network-manipulation-closeDiv';
  16945. this.closeDiv.id = 'network-manipulation-closeDiv';
  16946. this.closeDiv.style.display = this.manipulationDiv.style.display;
  16947. this.containerElement.insertBefore(this.closeDiv, this.frame);
  16948. }
  16949. // load the manipulation functions
  16950. this._loadMixin(manipulationMixin);
  16951. // create the manipulator toolbar
  16952. this._createManipulatorBar();
  16953. }
  16954. else {
  16955. if (this.manipulationDiv !== undefined) {
  16956. // removes all the bindings and overloads
  16957. this._createManipulatorBar();
  16958. // remove the manipulation divs
  16959. this.containerElement.removeChild(this.manipulationDiv);
  16960. this.containerElement.removeChild(this.editModeDiv);
  16961. this.containerElement.removeChild(this.closeDiv);
  16962. this.manipulationDiv = undefined;
  16963. this.editModeDiv = undefined;
  16964. this.closeDiv = undefined;
  16965. // remove the mixin functions
  16966. this._clearMixin(manipulationMixin);
  16967. }
  16968. }
  16969. },
  16970. /**
  16971. * Mixin the navigation (User Interface) system and initialize the parameters required
  16972. *
  16973. * @private
  16974. */
  16975. _loadNavigationControls: function () {
  16976. this._loadMixin(NavigationMixin);
  16977. // the clean function removes the button divs, this is done to remove the bindings.
  16978. this._cleanNavigation();
  16979. if (this.constants.navigation.enabled == true) {
  16980. this._loadNavigationElements();
  16981. }
  16982. },
  16983. /**
  16984. * Mixin the hierarchical layout system.
  16985. *
  16986. * @private
  16987. */
  16988. _loadHierarchySystem: function () {
  16989. this._loadMixin(HierarchicalLayoutMixin);
  16990. }
  16991. };
  16992. /**
  16993. * @constructor Network
  16994. * Create a network visualization, displaying nodes and edges.
  16995. *
  16996. * @param {Element} container The DOM element in which the Network will
  16997. * be created. Normally a div element.
  16998. * @param {Object} data An object containing parameters
  16999. * {Array} nodes
  17000. * {Array} edges
  17001. * @param {Object} options Options
  17002. */
  17003. function Network (container, data, options) {
  17004. if (!(this instanceof Network)) {
  17005. throw new SyntaxError('Constructor must be called with the new operator');
  17006. }
  17007. this._initializeMixinLoaders();
  17008. // create variables and set default values
  17009. this.containerElement = container;
  17010. this.width = '100%';
  17011. this.height = '100%';
  17012. // render and calculation settings
  17013. this.renderRefreshRate = 60; // hz (fps)
  17014. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  17015. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  17016. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  17017. this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
  17018. this.stabilize = true; // stabilize before displaying the network
  17019. this.selectable = true;
  17020. this.initializing = true;
  17021. // these functions are triggered when the dataset is edited
  17022. this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
  17023. // set constant values
  17024. this.constants = {
  17025. nodes: {
  17026. radiusMin: 5,
  17027. radiusMax: 20,
  17028. radius: 5,
  17029. shape: 'ellipse',
  17030. image: undefined,
  17031. widthMin: 16, // px
  17032. widthMax: 64, // px
  17033. fixed: false,
  17034. fontColor: 'black',
  17035. fontSize: 14, // px
  17036. fontFace: 'verdana',
  17037. level: -1,
  17038. color: {
  17039. border: '#2B7CE9',
  17040. background: '#97C2FC',
  17041. highlight: {
  17042. border: '#2B7CE9',
  17043. background: '#D2E5FF'
  17044. },
  17045. hover: {
  17046. border: '#2B7CE9',
  17047. background: '#D2E5FF'
  17048. }
  17049. },
  17050. borderColor: '#2B7CE9',
  17051. backgroundColor: '#97C2FC',
  17052. highlightColor: '#D2E5FF',
  17053. group: undefined
  17054. },
  17055. edges: {
  17056. widthMin: 1,
  17057. widthMax: 15,
  17058. width: 1,
  17059. widthSelectionMultiplier: 2,
  17060. hoverWidth: 1.5,
  17061. style: 'line',
  17062. color: {
  17063. color:'#848484',
  17064. highlight:'#848484',
  17065. hover: '#848484'
  17066. },
  17067. fontColor: '#343434',
  17068. fontSize: 14, // px
  17069. fontFace: 'arial',
  17070. fontFill: 'white',
  17071. arrowScaleFactor: 1,
  17072. dash: {
  17073. length: 10,
  17074. gap: 5,
  17075. altLength: undefined
  17076. }
  17077. },
  17078. configurePhysics:false,
  17079. physics: {
  17080. barnesHut: {
  17081. enabled: true,
  17082. theta: 1 / 0.6, // inverted to save time during calculation
  17083. gravitationalConstant: -2000,
  17084. centralGravity: 0.3,
  17085. springLength: 95,
  17086. springConstant: 0.04,
  17087. damping: 0.09
  17088. },
  17089. repulsion: {
  17090. centralGravity: 0.1,
  17091. springLength: 200,
  17092. springConstant: 0.05,
  17093. nodeDistance: 100,
  17094. damping: 0.09
  17095. },
  17096. hierarchicalRepulsion: {
  17097. enabled: false,
  17098. centralGravity: 0.5,
  17099. springLength: 150,
  17100. springConstant: 0.01,
  17101. nodeDistance: 60,
  17102. damping: 0.09
  17103. },
  17104. damping: null,
  17105. centralGravity: null,
  17106. springLength: null,
  17107. springConstant: null
  17108. },
  17109. clustering: { // Per Node in Cluster = PNiC
  17110. enabled: false, // (Boolean) | global on/off switch for clustering.
  17111. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  17112. 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
  17113. 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
  17114. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  17115. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  17116. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  17117. 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.
  17118. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  17119. maxFontSize: 1000,
  17120. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  17121. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  17122. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  17123. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  17124. height: 1, // (px PNiC) | growth of the height per node in cluster.
  17125. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  17126. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  17127. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  17128. clusterLevelDifference: 2
  17129. },
  17130. navigation: {
  17131. enabled: false
  17132. },
  17133. keyboard: {
  17134. enabled: false,
  17135. speed: {x: 10, y: 10, zoom: 0.02}
  17136. },
  17137. dataManipulation: {
  17138. enabled: false,
  17139. initiallyVisible: false
  17140. },
  17141. hierarchicalLayout: {
  17142. enabled:false,
  17143. levelSeparation: 150,
  17144. nodeSpacing: 100,
  17145. direction: "UD" // UD, DU, LR, RL
  17146. },
  17147. freezeForStabilization: false,
  17148. smoothCurves: true,
  17149. maxVelocity: 10,
  17150. minVelocity: 0.1, // px/s
  17151. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  17152. labels:{
  17153. add:"Add Node",
  17154. edit:"Edit",
  17155. link:"Add Link",
  17156. del:"Delete selected",
  17157. editNode:"Edit Node",
  17158. editEdge:"Edit Edge",
  17159. back:"Back",
  17160. addDescription:"Click in an empty space to place a new node.",
  17161. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  17162. editEdgeDescription:"Click on the control points and drag them to a node to connect to it.",
  17163. addError:"The function for add does not support two arguments (data,callback).",
  17164. linkError:"The function for connect does not support two arguments (data,callback).",
  17165. editError:"The function for edit does not support two arguments (data, callback).",
  17166. editBoundError:"No edit function has been bound to this button.",
  17167. deleteError:"The function for delete does not support two arguments (data, callback).",
  17168. deleteClusterError:"Clusters cannot be deleted."
  17169. },
  17170. tooltip: {
  17171. delay: 300,
  17172. fontColor: 'black',
  17173. fontSize: 14, // px
  17174. fontFace: 'verdana',
  17175. color: {
  17176. border: '#666',
  17177. background: '#FFFFC6'
  17178. }
  17179. },
  17180. dragNetwork: true,
  17181. dragNodes: true,
  17182. zoomable: true,
  17183. hover: false
  17184. };
  17185. this.hoverObj = {nodes:{},edges:{}};
  17186. // Node variables
  17187. var network = this;
  17188. this.groups = new Groups(); // object with groups
  17189. this.images = new Images(); // object with images
  17190. this.images.setOnloadCallback(function () {
  17191. network._redraw();
  17192. });
  17193. // keyboard navigation variables
  17194. this.xIncrement = 0;
  17195. this.yIncrement = 0;
  17196. this.zoomIncrement = 0;
  17197. // loading all the mixins:
  17198. // load the force calculation functions, grouped under the physics system.
  17199. this._loadPhysicsSystem();
  17200. // create a frame and canvas
  17201. this._create();
  17202. // load the sector system. (mandatory, fully integrated with Network)
  17203. this._loadSectorSystem();
  17204. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  17205. this._loadClusterSystem();
  17206. // load the selection system. (mandatory, required by Network)
  17207. this._loadSelectionSystem();
  17208. // load the selection system. (mandatory, required by Network)
  17209. this._loadHierarchySystem();
  17210. // apply options
  17211. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  17212. this._setScale(1);
  17213. this.setOptions(options);
  17214. // other vars
  17215. this.freezeSimulation = false;// freeze the simulation
  17216. this.cachedFunctions = {};
  17217. // containers for nodes and edges
  17218. this.calculationNodes = {};
  17219. this.calculationNodeIndices = [];
  17220. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  17221. this.nodes = {}; // object with Node objects
  17222. this.edges = {}; // object with Edge objects
  17223. // position and scale variables and objects
  17224. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  17225. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  17226. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  17227. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  17228. this.scale = 1; // defining the global scale variable in the constructor
  17229. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  17230. // datasets or dataviews
  17231. this.nodesData = null; // A DataSet or DataView
  17232. this.edgesData = null; // A DataSet or DataView
  17233. // create event listeners used to subscribe on the DataSets of the nodes and edges
  17234. this.nodesListeners = {
  17235. 'add': function (event, params) {
  17236. network._addNodes(params.items);
  17237. network.start();
  17238. },
  17239. 'update': function (event, params) {
  17240. network._updateNodes(params.items);
  17241. network.start();
  17242. },
  17243. 'remove': function (event, params) {
  17244. network._removeNodes(params.items);
  17245. network.start();
  17246. }
  17247. };
  17248. this.edgesListeners = {
  17249. 'add': function (event, params) {
  17250. network._addEdges(params.items);
  17251. network.start();
  17252. },
  17253. 'update': function (event, params) {
  17254. network._updateEdges(params.items);
  17255. network.start();
  17256. },
  17257. 'remove': function (event, params) {
  17258. network._removeEdges(params.items);
  17259. network.start();
  17260. }
  17261. };
  17262. // properties for the animation
  17263. this.moving = true;
  17264. this.timer = undefined; // Scheduling function. Is definded in this.start();
  17265. // load data (the disable start variable will be the same as the enabled clustering)
  17266. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  17267. // hierarchical layout
  17268. this.initializing = false;
  17269. if (this.constants.hierarchicalLayout.enabled == true) {
  17270. this._setupHierarchicalLayout();
  17271. }
  17272. else {
  17273. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  17274. if (this.stabilize == false) {
  17275. this.zoomExtent(true,this.constants.clustering.enabled);
  17276. }
  17277. }
  17278. // if clustering is disabled, the simulation will have started in the setData function
  17279. if (this.constants.clustering.enabled) {
  17280. this.startWithClustering();
  17281. }
  17282. }
  17283. // Extend Network with an Emitter mixin
  17284. Emitter(Network.prototype);
  17285. /**
  17286. * Get the script path where the vis.js library is located
  17287. *
  17288. * @returns {string | null} path Path or null when not found. Path does not
  17289. * end with a slash.
  17290. * @private
  17291. */
  17292. Network.prototype._getScriptPath = function() {
  17293. var scripts = document.getElementsByTagName( 'script' );
  17294. // find script named vis.js or vis.min.js
  17295. for (var i = 0; i < scripts.length; i++) {
  17296. var src = scripts[i].src;
  17297. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  17298. if (match) {
  17299. // return path without the script name
  17300. return src.substring(0, src.length - match[0].length);
  17301. }
  17302. }
  17303. return null;
  17304. };
  17305. /**
  17306. * Find the center position of the network
  17307. * @private
  17308. */
  17309. Network.prototype._getRange = function() {
  17310. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  17311. for (var nodeId in this.nodes) {
  17312. if (this.nodes.hasOwnProperty(nodeId)) {
  17313. node = this.nodes[nodeId];
  17314. if (minX > (node.x)) {minX = node.x;}
  17315. if (maxX < (node.x)) {maxX = node.x;}
  17316. if (minY > (node.y)) {minY = node.y;}
  17317. if (maxY < (node.y)) {maxY = node.y;}
  17318. }
  17319. }
  17320. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  17321. minY = 0, maxY = 0, minX = 0, maxX = 0;
  17322. }
  17323. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  17324. };
  17325. /**
  17326. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  17327. * @returns {{x: number, y: number}}
  17328. * @private
  17329. */
  17330. Network.prototype._findCenter = function(range) {
  17331. return {x: (0.5 * (range.maxX + range.minX)),
  17332. y: (0.5 * (range.maxY + range.minY))};
  17333. };
  17334. /**
  17335. * center the network
  17336. *
  17337. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  17338. */
  17339. Network.prototype._centerNetwork = function(range) {
  17340. var center = this._findCenter(range);
  17341. center.x *= this.scale;
  17342. center.y *= this.scale;
  17343. center.x -= 0.5 * this.frame.canvas.clientWidth;
  17344. center.y -= 0.5 * this.frame.canvas.clientHeight;
  17345. this._setTranslation(-center.x,-center.y); // set at 0,0
  17346. };
  17347. /**
  17348. * This function zooms out to fit all data on screen based on amount of nodes
  17349. *
  17350. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  17351. * @param {Boolean} [disableStart] | If true, start is not called.
  17352. */
  17353. Network.prototype.zoomExtent = function(initialZoom, disableStart) {
  17354. if (initialZoom === undefined) {
  17355. initialZoom = false;
  17356. }
  17357. if (disableStart === undefined) {
  17358. disableStart = false;
  17359. }
  17360. var range = this._getRange();
  17361. var zoomLevel;
  17362. if (initialZoom == true) {
  17363. var numberOfNodes = this.nodeIndices.length;
  17364. if (this.constants.smoothCurves == true) {
  17365. if (this.constants.clustering.enabled == true &&
  17366. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  17367. 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.
  17368. }
  17369. else {
  17370. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  17371. }
  17372. }
  17373. else {
  17374. if (this.constants.clustering.enabled == true &&
  17375. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  17376. 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.
  17377. }
  17378. else {
  17379. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  17380. }
  17381. }
  17382. // correct for larger canvasses.
  17383. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  17384. zoomLevel *= factor;
  17385. }
  17386. else {
  17387. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  17388. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  17389. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  17390. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  17391. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  17392. }
  17393. if (zoomLevel > 1.0) {
  17394. zoomLevel = 1.0;
  17395. }
  17396. this._setScale(zoomLevel);
  17397. this._centerNetwork(range);
  17398. if (disableStart == false) {
  17399. this.moving = true;
  17400. this.start();
  17401. }
  17402. };
  17403. /**
  17404. * Update the this.nodeIndices with the most recent node index list
  17405. * @private
  17406. */
  17407. Network.prototype._updateNodeIndexList = function() {
  17408. this._clearNodeIndexList();
  17409. for (var idx in this.nodes) {
  17410. if (this.nodes.hasOwnProperty(idx)) {
  17411. this.nodeIndices.push(idx);
  17412. }
  17413. }
  17414. };
  17415. /**
  17416. * Set nodes and edges, and optionally options as well.
  17417. *
  17418. * @param {Object} data Object containing parameters:
  17419. * {Array | DataSet | DataView} [nodes] Array with nodes
  17420. * {Array | DataSet | DataView} [edges] Array with edges
  17421. * {String} [dot] String containing data in DOT format
  17422. * {Options} [options] Object with options
  17423. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  17424. */
  17425. Network.prototype.setData = function(data, disableStart) {
  17426. if (disableStart === undefined) {
  17427. disableStart = false;
  17428. }
  17429. if (data && data.dot && (data.nodes || data.edges)) {
  17430. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  17431. ' parameter pair "nodes" and "edges", but not both.');
  17432. }
  17433. // set options
  17434. this.setOptions(data && data.options);
  17435. // set all data
  17436. if (data && data.dot) {
  17437. // parse DOT file
  17438. if(data && data.dot) {
  17439. var dotData = vis.util.DOTToGraph(data.dot);
  17440. this.setData(dotData);
  17441. return;
  17442. }
  17443. }
  17444. else {
  17445. this._setNodes(data && data.nodes);
  17446. this._setEdges(data && data.edges);
  17447. }
  17448. this._putDataInSector();
  17449. if (!disableStart) {
  17450. // find a stable position or start animating to a stable position
  17451. if (this.stabilize) {
  17452. var me = this;
  17453. setTimeout(function() {me._stabilize(); me.start();},0)
  17454. }
  17455. else {
  17456. this.start();
  17457. }
  17458. }
  17459. };
  17460. /**
  17461. * Set options
  17462. * @param {Object} options
  17463. * @param {Boolean} [initializeView] | set zoom and translation to default.
  17464. */
  17465. Network.prototype.setOptions = function (options) {
  17466. if (options) {
  17467. var prop;
  17468. // retrieve parameter values
  17469. if (options.width !== undefined) {this.width = options.width;}
  17470. if (options.height !== undefined) {this.height = options.height;}
  17471. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  17472. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  17473. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  17474. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  17475. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  17476. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  17477. if (options.dragNetwork !== undefined) {this.constants.dragNetwork = options.dragNetwork;}
  17478. if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;}
  17479. if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
  17480. if (options.hover !== undefined) {this.constants.hover = options.hover;}
  17481. // TODO: deprecated since version 3.0.0. Cleanup some day
  17482. if (options.dragGraph !== undefined) {
  17483. throw new Error('Option dragGraph is renamed to dragNetwork');
  17484. }
  17485. if (options.labels !== undefined) {
  17486. for (prop in options.labels) {
  17487. if (options.labels.hasOwnProperty(prop)) {
  17488. this.constants.labels[prop] = options.labels[prop];
  17489. }
  17490. }
  17491. }
  17492. if (options.onAdd) {
  17493. this.triggerFunctions.add = options.onAdd;
  17494. }
  17495. if (options.onEdit) {
  17496. this.triggerFunctions.edit = options.onEdit;
  17497. }
  17498. if (options.onEditEdge) {
  17499. this.triggerFunctions.editEdge = options.onEditEdge;
  17500. }
  17501. if (options.onConnect) {
  17502. this.triggerFunctions.connect = options.onConnect;
  17503. }
  17504. if (options.onDelete) {
  17505. this.triggerFunctions.del = options.onDelete;
  17506. }
  17507. if (options.physics) {
  17508. if (options.physics.barnesHut) {
  17509. this.constants.physics.barnesHut.enabled = true;
  17510. for (prop in options.physics.barnesHut) {
  17511. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  17512. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  17513. }
  17514. }
  17515. }
  17516. if (options.physics.repulsion) {
  17517. this.constants.physics.barnesHut.enabled = false;
  17518. for (prop in options.physics.repulsion) {
  17519. if (options.physics.repulsion.hasOwnProperty(prop)) {
  17520. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  17521. }
  17522. }
  17523. }
  17524. if (options.physics.hierarchicalRepulsion) {
  17525. this.constants.hierarchicalLayout.enabled = true;
  17526. this.constants.physics.hierarchicalRepulsion.enabled = true;
  17527. this.constants.physics.barnesHut.enabled = false;
  17528. for (prop in options.physics.hierarchicalRepulsion) {
  17529. if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
  17530. this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
  17531. }
  17532. }
  17533. }
  17534. }
  17535. if (options.hierarchicalLayout) {
  17536. this.constants.hierarchicalLayout.enabled = true;
  17537. for (prop in options.hierarchicalLayout) {
  17538. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  17539. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  17540. }
  17541. }
  17542. }
  17543. else if (options.hierarchicalLayout !== undefined) {
  17544. this.constants.hierarchicalLayout.enabled = false;
  17545. }
  17546. if (options.clustering) {
  17547. this.constants.clustering.enabled = true;
  17548. for (prop in options.clustering) {
  17549. if (options.clustering.hasOwnProperty(prop)) {
  17550. this.constants.clustering[prop] = options.clustering[prop];
  17551. }
  17552. }
  17553. }
  17554. else if (options.clustering !== undefined) {
  17555. this.constants.clustering.enabled = false;
  17556. }
  17557. if (options.navigation) {
  17558. this.constants.navigation.enabled = true;
  17559. for (prop in options.navigation) {
  17560. if (options.navigation.hasOwnProperty(prop)) {
  17561. this.constants.navigation[prop] = options.navigation[prop];
  17562. }
  17563. }
  17564. }
  17565. else if (options.navigation !== undefined) {
  17566. this.constants.navigation.enabled = false;
  17567. }
  17568. if (options.keyboard) {
  17569. this.constants.keyboard.enabled = true;
  17570. for (prop in options.keyboard) {
  17571. if (options.keyboard.hasOwnProperty(prop)) {
  17572. this.constants.keyboard[prop] = options.keyboard[prop];
  17573. }
  17574. }
  17575. }
  17576. else if (options.keyboard !== undefined) {
  17577. this.constants.keyboard.enabled = false;
  17578. }
  17579. if (options.dataManipulation) {
  17580. this.constants.dataManipulation.enabled = true;
  17581. for (prop in options.dataManipulation) {
  17582. if (options.dataManipulation.hasOwnProperty(prop)) {
  17583. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  17584. }
  17585. }
  17586. this.editMode = this.constants.dataManipulation.initiallyVisible;
  17587. }
  17588. else if (options.dataManipulation !== undefined) {
  17589. this.constants.dataManipulation.enabled = false;
  17590. }
  17591. // TODO: work out these options and document them
  17592. if (options.edges) {
  17593. for (prop in options.edges) {
  17594. if (options.edges.hasOwnProperty(prop)) {
  17595. if (typeof options.edges[prop] != "object") {
  17596. this.constants.edges[prop] = options.edges[prop];
  17597. }
  17598. }
  17599. }
  17600. if (options.edges.color !== undefined) {
  17601. if (util.isString(options.edges.color)) {
  17602. this.constants.edges.color = {};
  17603. this.constants.edges.color.color = options.edges.color;
  17604. this.constants.edges.color.highlight = options.edges.color;
  17605. this.constants.edges.color.hover = options.edges.color;
  17606. }
  17607. else {
  17608. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  17609. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  17610. if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
  17611. }
  17612. }
  17613. if (!options.edges.fontColor) {
  17614. if (options.edges.color !== undefined) {
  17615. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  17616. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  17617. }
  17618. }
  17619. // Added to support dashed lines
  17620. // David Jordan
  17621. // 2012-08-08
  17622. if (options.edges.dash) {
  17623. if (options.edges.dash.length !== undefined) {
  17624. this.constants.edges.dash.length = options.edges.dash.length;
  17625. }
  17626. if (options.edges.dash.gap !== undefined) {
  17627. this.constants.edges.dash.gap = options.edges.dash.gap;
  17628. }
  17629. if (options.edges.dash.altLength !== undefined) {
  17630. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  17631. }
  17632. }
  17633. }
  17634. if (options.nodes) {
  17635. for (prop in options.nodes) {
  17636. if (options.nodes.hasOwnProperty(prop)) {
  17637. this.constants.nodes[prop] = options.nodes[prop];
  17638. }
  17639. }
  17640. if (options.nodes.color) {
  17641. this.constants.nodes.color = util.parseColor(options.nodes.color);
  17642. }
  17643. /*
  17644. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  17645. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  17646. */
  17647. }
  17648. if (options.groups) {
  17649. for (var groupname in options.groups) {
  17650. if (options.groups.hasOwnProperty(groupname)) {
  17651. var group = options.groups[groupname];
  17652. this.groups.add(groupname, group);
  17653. }
  17654. }
  17655. }
  17656. if (options.tooltip) {
  17657. for (prop in options.tooltip) {
  17658. if (options.tooltip.hasOwnProperty(prop)) {
  17659. this.constants.tooltip[prop] = options.tooltip[prop];
  17660. }
  17661. }
  17662. if (options.tooltip.color) {
  17663. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  17664. }
  17665. }
  17666. }
  17667. // (Re)loading the mixins that can be enabled or disabled in the options.
  17668. // load the force calculation functions, grouped under the physics system.
  17669. this._loadPhysicsSystem();
  17670. // load the navigation system.
  17671. this._loadNavigationControls();
  17672. // load the data manipulation system
  17673. this._loadManipulationSystem();
  17674. // configure the smooth curves
  17675. this._configureSmoothCurves();
  17676. // bind keys. If disabled, this will not do anything;
  17677. this._createKeyBinds();
  17678. this.setSize(this.width, this.height);
  17679. this.moving = true;
  17680. this.start();
  17681. };
  17682. /**
  17683. * Create the main frame for the Network.
  17684. * This function is executed once when a Network object is created. The frame
  17685. * contains a canvas, and this canvas contains all objects like the axis and
  17686. * nodes.
  17687. * @private
  17688. */
  17689. Network.prototype._create = function () {
  17690. // remove all elements from the container element.
  17691. while (this.containerElement.hasChildNodes()) {
  17692. this.containerElement.removeChild(this.containerElement.firstChild);
  17693. }
  17694. this.frame = document.createElement('div');
  17695. this.frame.className = 'network-frame';
  17696. this.frame.style.position = 'relative';
  17697. this.frame.style.overflow = 'hidden';
  17698. // create the network canvas (HTML canvas element)
  17699. this.frame.canvas = document.createElement( 'canvas' );
  17700. this.frame.canvas.style.position = 'relative';
  17701. this.frame.appendChild(this.frame.canvas);
  17702. if (!this.frame.canvas.getContext) {
  17703. var noCanvas = document.createElement( 'DIV' );
  17704. noCanvas.style.color = 'red';
  17705. noCanvas.style.fontWeight = 'bold' ;
  17706. noCanvas.style.padding = '10px';
  17707. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  17708. this.frame.canvas.appendChild(noCanvas);
  17709. }
  17710. var me = this;
  17711. this.drag = {};
  17712. this.pinch = {};
  17713. this.hammer = Hammer(this.frame.canvas, {
  17714. prevent_default: true
  17715. });
  17716. this.hammer.on('tap', me._onTap.bind(me) );
  17717. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  17718. this.hammer.on('hold', me._onHold.bind(me) );
  17719. this.hammer.on('pinch', me._onPinch.bind(me) );
  17720. this.hammer.on('touch', me._onTouch.bind(me) );
  17721. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  17722. this.hammer.on('drag', me._onDrag.bind(me) );
  17723. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  17724. this.hammer.on('release', me._onRelease.bind(me) );
  17725. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  17726. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  17727. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  17728. // add the frame to the container element
  17729. this.containerElement.appendChild(this.frame);
  17730. };
  17731. /**
  17732. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  17733. * @private
  17734. */
  17735. Network.prototype._createKeyBinds = function() {
  17736. var me = this;
  17737. this.mousetrap = mousetrap;
  17738. this.mousetrap.reset();
  17739. if (this.constants.keyboard.enabled == true) {
  17740. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  17741. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  17742. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  17743. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  17744. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  17745. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  17746. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  17747. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  17748. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  17749. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  17750. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  17751. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  17752. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  17753. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  17754. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  17755. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  17756. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  17757. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  17758. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  17759. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  17760. }
  17761. if (this.constants.dataManipulation.enabled == true) {
  17762. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  17763. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  17764. }
  17765. };
  17766. /**
  17767. * Get the pointer location from a touch location
  17768. * @param {{pageX: Number, pageY: Number}} touch
  17769. * @return {{x: Number, y: Number}} pointer
  17770. * @private
  17771. */
  17772. Network.prototype._getPointer = function (touch) {
  17773. return {
  17774. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  17775. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  17776. };
  17777. };
  17778. /**
  17779. * On start of a touch gesture, store the pointer
  17780. * @param event
  17781. * @private
  17782. */
  17783. Network.prototype._onTouch = function (event) {
  17784. this.drag.pointer = this._getPointer(event.gesture.center);
  17785. this.drag.pinched = false;
  17786. this.pinch.scale = this._getScale();
  17787. this._handleTouch(this.drag.pointer);
  17788. };
  17789. /**
  17790. * handle drag start event
  17791. * @private
  17792. */
  17793. Network.prototype._onDragStart = function () {
  17794. this._handleDragStart();
  17795. };
  17796. /**
  17797. * This function is called by _onDragStart.
  17798. * It is separated out because we can then overload it for the datamanipulation system.
  17799. *
  17800. * @private
  17801. */
  17802. Network.prototype._handleDragStart = function() {
  17803. var drag = this.drag;
  17804. var node = this._getNodeAt(drag.pointer);
  17805. // note: drag.pointer is set in _onTouch to get the initial touch location
  17806. drag.dragging = true;
  17807. drag.selection = [];
  17808. drag.translation = this._getTranslation();
  17809. drag.nodeId = null;
  17810. if (node != null) {
  17811. drag.nodeId = node.id;
  17812. // select the clicked node if not yet selected
  17813. if (!node.isSelected()) {
  17814. this._selectObject(node,false);
  17815. }
  17816. // create an array with the selected nodes and their original location and status
  17817. for (var objectId in this.selectionObj.nodes) {
  17818. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  17819. var object = this.selectionObj.nodes[objectId];
  17820. var s = {
  17821. id: object.id,
  17822. node: object,
  17823. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  17824. x: object.x,
  17825. y: object.y,
  17826. xFixed: object.xFixed,
  17827. yFixed: object.yFixed
  17828. };
  17829. object.xFixed = true;
  17830. object.yFixed = true;
  17831. drag.selection.push(s);
  17832. }
  17833. }
  17834. }
  17835. };
  17836. /**
  17837. * handle drag event
  17838. * @private
  17839. */
  17840. Network.prototype._onDrag = function (event) {
  17841. this._handleOnDrag(event)
  17842. };
  17843. /**
  17844. * This function is called by _onDrag.
  17845. * It is separated out because we can then overload it for the datamanipulation system.
  17846. *
  17847. * @private
  17848. */
  17849. Network.prototype._handleOnDrag = function(event) {
  17850. if (this.drag.pinched) {
  17851. return;
  17852. }
  17853. var pointer = this._getPointer(event.gesture.center);
  17854. var me = this,
  17855. drag = this.drag,
  17856. selection = drag.selection;
  17857. if (selection && selection.length && this.constants.dragNodes == true) {
  17858. // calculate delta's and new location
  17859. var deltaX = pointer.x - drag.pointer.x,
  17860. deltaY = pointer.y - drag.pointer.y;
  17861. // update position of all selected nodes
  17862. selection.forEach(function (s) {
  17863. var node = s.node;
  17864. if (!s.xFixed) {
  17865. node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
  17866. }
  17867. if (!s.yFixed) {
  17868. node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
  17869. }
  17870. });
  17871. // start _animationStep if not yet running
  17872. if (!this.moving) {
  17873. this.moving = true;
  17874. this.start();
  17875. }
  17876. }
  17877. else {
  17878. if (this.constants.dragNetwork == true) {
  17879. // move the network
  17880. var diffX = pointer.x - this.drag.pointer.x;
  17881. var diffY = pointer.y - this.drag.pointer.y;
  17882. this._setTranslation(
  17883. this.drag.translation.x + diffX,
  17884. this.drag.translation.y + diffY);
  17885. this._redraw();
  17886. this.moving = true;
  17887. this.start();
  17888. }
  17889. }
  17890. };
  17891. /**
  17892. * handle drag start event
  17893. * @private
  17894. */
  17895. Network.prototype._onDragEnd = function () {
  17896. this.drag.dragging = false;
  17897. var selection = this.drag.selection;
  17898. if (selection) {
  17899. selection.forEach(function (s) {
  17900. // restore original xFixed and yFixed
  17901. s.node.xFixed = s.xFixed;
  17902. s.node.yFixed = s.yFixed;
  17903. });
  17904. }
  17905. };
  17906. /**
  17907. * handle tap/click event: select/unselect a node
  17908. * @private
  17909. */
  17910. Network.prototype._onTap = function (event) {
  17911. var pointer = this._getPointer(event.gesture.center);
  17912. this.pointerPosition = pointer;
  17913. this._handleTap(pointer);
  17914. };
  17915. /**
  17916. * handle doubletap event
  17917. * @private
  17918. */
  17919. Network.prototype._onDoubleTap = function (event) {
  17920. var pointer = this._getPointer(event.gesture.center);
  17921. this._handleDoubleTap(pointer);
  17922. };
  17923. /**
  17924. * handle long tap event: multi select nodes
  17925. * @private
  17926. */
  17927. Network.prototype._onHold = function (event) {
  17928. var pointer = this._getPointer(event.gesture.center);
  17929. this.pointerPosition = pointer;
  17930. this._handleOnHold(pointer);
  17931. };
  17932. /**
  17933. * handle the release of the screen
  17934. *
  17935. * @private
  17936. */
  17937. Network.prototype._onRelease = function (event) {
  17938. var pointer = this._getPointer(event.gesture.center);
  17939. this._handleOnRelease(pointer);
  17940. };
  17941. /**
  17942. * Handle pinch event
  17943. * @param event
  17944. * @private
  17945. */
  17946. Network.prototype._onPinch = function (event) {
  17947. var pointer = this._getPointer(event.gesture.center);
  17948. this.drag.pinched = true;
  17949. if (!('scale' in this.pinch)) {
  17950. this.pinch.scale = 1;
  17951. }
  17952. // TODO: enabled moving while pinching?
  17953. var scale = this.pinch.scale * event.gesture.scale;
  17954. this._zoom(scale, pointer)
  17955. };
  17956. /**
  17957. * Zoom the network in or out
  17958. * @param {Number} scale a number around 1, and between 0.01 and 10
  17959. * @param {{x: Number, y: Number}} pointer Position on screen
  17960. * @return {Number} appliedScale scale is limited within the boundaries
  17961. * @private
  17962. */
  17963. Network.prototype._zoom = function(scale, pointer) {
  17964. if (this.constants.zoomable == true) {
  17965. var scaleOld = this._getScale();
  17966. if (scale < 0.00001) {
  17967. scale = 0.00001;
  17968. }
  17969. if (scale > 10) {
  17970. scale = 10;
  17971. }
  17972. // + this.frame.canvas.clientHeight / 2
  17973. var translation = this._getTranslation();
  17974. var scaleFrac = scale / scaleOld;
  17975. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  17976. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  17977. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  17978. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  17979. this._setScale(scale);
  17980. this._setTranslation(tx, ty);
  17981. this.updateClustersDefault();
  17982. this._redraw();
  17983. if (scaleOld < scale) {
  17984. this.emit("zoom", {direction:"+"});
  17985. }
  17986. else {
  17987. this.emit("zoom", {direction:"-"});
  17988. }
  17989. return scale;
  17990. }
  17991. };
  17992. /**
  17993. * Event handler for mouse wheel event, used to zoom the timeline
  17994. * See http://adomas.org/javascript-mouse-wheel/
  17995. * https://github.com/EightMedia/hammer.js/issues/256
  17996. * @param {MouseEvent} event
  17997. * @private
  17998. */
  17999. Network.prototype._onMouseWheel = function(event) {
  18000. // retrieve delta
  18001. var delta = 0;
  18002. if (event.wheelDelta) { /* IE/Opera. */
  18003. delta = event.wheelDelta/120;
  18004. } else if (event.detail) { /* Mozilla case. */
  18005. // In Mozilla, sign of delta is different than in IE.
  18006. // Also, delta is multiple of 3.
  18007. delta = -event.detail/3;
  18008. }
  18009. // If delta is nonzero, handle it.
  18010. // Basically, delta is now positive if wheel was scrolled up,
  18011. // and negative, if wheel was scrolled down.
  18012. if (delta) {
  18013. // calculate the new scale
  18014. var scale = this._getScale();
  18015. var zoom = delta / 10;
  18016. if (delta < 0) {
  18017. zoom = zoom / (1 - zoom);
  18018. }
  18019. scale *= (1 + zoom);
  18020. // calculate the pointer location
  18021. var gesture = util.fakeGesture(this, event);
  18022. var pointer = this._getPointer(gesture.center);
  18023. // apply the new scale
  18024. this._zoom(scale, pointer);
  18025. }
  18026. // Prevent default actions caused by mouse wheel.
  18027. event.preventDefault();
  18028. };
  18029. /**
  18030. * Mouse move handler for checking whether the title moves over a node with a title.
  18031. * @param {Event} event
  18032. * @private
  18033. */
  18034. Network.prototype._onMouseMoveTitle = function (event) {
  18035. var gesture = util.fakeGesture(this, event);
  18036. var pointer = this._getPointer(gesture.center);
  18037. // check if the previously selected node is still selected
  18038. if (this.popupObj) {
  18039. this._checkHidePopup(pointer);
  18040. }
  18041. // start a timeout that will check if the mouse is positioned above
  18042. // an element
  18043. var me = this;
  18044. var checkShow = function() {
  18045. me._checkShowPopup(pointer);
  18046. };
  18047. if (this.popupTimer) {
  18048. clearInterval(this.popupTimer); // stop any running calculationTimer
  18049. }
  18050. if (!this.drag.dragging) {
  18051. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  18052. }
  18053. /**
  18054. * Adding hover highlights
  18055. */
  18056. if (this.constants.hover == true) {
  18057. // removing all hover highlights
  18058. for (var edgeId in this.hoverObj.edges) {
  18059. if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  18060. this.hoverObj.edges[edgeId].hover = false;
  18061. delete this.hoverObj.edges[edgeId];
  18062. }
  18063. }
  18064. // adding hover highlights
  18065. var obj = this._getNodeAt(pointer);
  18066. if (obj == null) {
  18067. obj = this._getEdgeAt(pointer);
  18068. }
  18069. if (obj != null) {
  18070. this._hoverObject(obj);
  18071. }
  18072. // removing all node hover highlights except for the selected one.
  18073. for (var nodeId in this.hoverObj.nodes) {
  18074. if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  18075. if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
  18076. this._blurObject(this.hoverObj.nodes[nodeId]);
  18077. delete this.hoverObj.nodes[nodeId];
  18078. }
  18079. }
  18080. }
  18081. this.redraw();
  18082. }
  18083. };
  18084. /**
  18085. * Check if there is an element on the given position in the network
  18086. * (a node or edge). If so, and if this element has a title,
  18087. * show a popup window with its title.
  18088. *
  18089. * @param {{x:Number, y:Number}} pointer
  18090. * @private
  18091. */
  18092. Network.prototype._checkShowPopup = function (pointer) {
  18093. var obj = {
  18094. left: this._XconvertDOMtoCanvas(pointer.x),
  18095. top: this._YconvertDOMtoCanvas(pointer.y),
  18096. right: this._XconvertDOMtoCanvas(pointer.x),
  18097. bottom: this._YconvertDOMtoCanvas(pointer.y)
  18098. };
  18099. var id;
  18100. var lastPopupNode = this.popupObj;
  18101. if (this.popupObj == undefined) {
  18102. // search the nodes for overlap, select the top one in case of multiple nodes
  18103. var nodes = this.nodes;
  18104. for (id in nodes) {
  18105. if (nodes.hasOwnProperty(id)) {
  18106. var node = nodes[id];
  18107. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  18108. this.popupObj = node;
  18109. break;
  18110. }
  18111. }
  18112. }
  18113. }
  18114. if (this.popupObj === undefined) {
  18115. // search the edges for overlap
  18116. var edges = this.edges;
  18117. for (id in edges) {
  18118. if (edges.hasOwnProperty(id)) {
  18119. var edge = edges[id];
  18120. if (edge.connected && (edge.getTitle() !== undefined) &&
  18121. edge.isOverlappingWith(obj)) {
  18122. this.popupObj = edge;
  18123. break;
  18124. }
  18125. }
  18126. }
  18127. }
  18128. if (this.popupObj) {
  18129. // show popup message window
  18130. if (this.popupObj != lastPopupNode) {
  18131. var me = this;
  18132. if (!me.popup) {
  18133. me.popup = new Popup(me.frame, me.constants.tooltip);
  18134. }
  18135. // adjust a small offset such that the mouse cursor is located in the
  18136. // bottom left location of the popup, and you can easily move over the
  18137. // popup area
  18138. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  18139. me.popup.setText(me.popupObj.getTitle());
  18140. me.popup.show();
  18141. }
  18142. }
  18143. else {
  18144. if (this.popup) {
  18145. this.popup.hide();
  18146. }
  18147. }
  18148. };
  18149. /**
  18150. * Check if the popup must be hided, which is the case when the mouse is no
  18151. * longer hovering on the object
  18152. * @param {{x:Number, y:Number}} pointer
  18153. * @private
  18154. */
  18155. Network.prototype._checkHidePopup = function (pointer) {
  18156. if (!this.popupObj || !this._getNodeAt(pointer) ) {
  18157. this.popupObj = undefined;
  18158. if (this.popup) {
  18159. this.popup.hide();
  18160. }
  18161. }
  18162. };
  18163. /**
  18164. * Set a new size for the network
  18165. * @param {string} width Width in pixels or percentage (for example '800px'
  18166. * or '50%')
  18167. * @param {string} height Height in pixels or percentage (for example '400px'
  18168. * or '30%')
  18169. */
  18170. Network.prototype.setSize = function(width, height) {
  18171. this.frame.style.width = width;
  18172. this.frame.style.height = height;
  18173. this.frame.canvas.style.width = '100%';
  18174. this.frame.canvas.style.height = '100%';
  18175. this.frame.canvas.width = this.frame.canvas.clientWidth;
  18176. this.frame.canvas.height = this.frame.canvas.clientHeight;
  18177. if (this.manipulationDiv !== undefined) {
  18178. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  18179. }
  18180. if (this.navigationDivs !== undefined) {
  18181. if (this.navigationDivs['wrapper'] !== undefined) {
  18182. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  18183. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  18184. }
  18185. }
  18186. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  18187. };
  18188. /**
  18189. * Set a data set with nodes for the network
  18190. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  18191. * @private
  18192. */
  18193. Network.prototype._setNodes = function(nodes) {
  18194. var oldNodesData = this.nodesData;
  18195. if (nodes instanceof DataSet || nodes instanceof DataView) {
  18196. this.nodesData = nodes;
  18197. }
  18198. else if (nodes instanceof Array) {
  18199. this.nodesData = new DataSet();
  18200. this.nodesData.add(nodes);
  18201. }
  18202. else if (!nodes) {
  18203. this.nodesData = new DataSet();
  18204. }
  18205. else {
  18206. throw new TypeError('Array or DataSet expected');
  18207. }
  18208. if (oldNodesData) {
  18209. // unsubscribe from old dataset
  18210. util.forEach(this.nodesListeners, function (callback, event) {
  18211. oldNodesData.off(event, callback);
  18212. });
  18213. }
  18214. // remove drawn nodes
  18215. this.nodes = {};
  18216. if (this.nodesData) {
  18217. // subscribe to new dataset
  18218. var me = this;
  18219. util.forEach(this.nodesListeners, function (callback, event) {
  18220. me.nodesData.on(event, callback);
  18221. });
  18222. // draw all new nodes
  18223. var ids = this.nodesData.getIds();
  18224. this._addNodes(ids);
  18225. }
  18226. this._updateSelection();
  18227. };
  18228. /**
  18229. * Add nodes
  18230. * @param {Number[] | String[]} ids
  18231. * @private
  18232. */
  18233. Network.prototype._addNodes = function(ids) {
  18234. var id;
  18235. for (var i = 0, len = ids.length; i < len; i++) {
  18236. id = ids[i];
  18237. var data = this.nodesData.get(id);
  18238. var node = new Node(data, this.images, this.groups, this.constants);
  18239. this.nodes[id] = node; // note: this may replace an existing node
  18240. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  18241. var radius = 10 * 0.1*ids.length;
  18242. var angle = 2 * Math.PI * Math.random();
  18243. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  18244. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  18245. }
  18246. this.moving = true;
  18247. }
  18248. this._updateNodeIndexList();
  18249. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18250. this._resetLevels();
  18251. this._setupHierarchicalLayout();
  18252. }
  18253. this._updateCalculationNodes();
  18254. this._reconnectEdges();
  18255. this._updateValueRange(this.nodes);
  18256. this.updateLabels();
  18257. };
  18258. /**
  18259. * Update existing nodes, or create them when not yet existing
  18260. * @param {Number[] | String[]} ids
  18261. * @private
  18262. */
  18263. Network.prototype._updateNodes = function(ids) {
  18264. var nodes = this.nodes,
  18265. nodesData = this.nodesData;
  18266. for (var i = 0, len = ids.length; i < len; i++) {
  18267. var id = ids[i];
  18268. var node = nodes[id];
  18269. var data = nodesData.get(id);
  18270. if (node) {
  18271. // update node
  18272. node.setProperties(data, this.constants);
  18273. }
  18274. else {
  18275. // create node
  18276. node = new Node(properties, this.images, this.groups, this.constants);
  18277. nodes[id] = node;
  18278. }
  18279. }
  18280. this.moving = true;
  18281. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18282. this._resetLevels();
  18283. this._setupHierarchicalLayout();
  18284. }
  18285. this._updateNodeIndexList();
  18286. this._reconnectEdges();
  18287. this._updateValueRange(nodes);
  18288. };
  18289. /**
  18290. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  18291. * @param {Number[] | String[]} ids
  18292. * @private
  18293. */
  18294. Network.prototype._removeNodes = function(ids) {
  18295. var nodes = this.nodes;
  18296. for (var i = 0, len = ids.length; i < len; i++) {
  18297. var id = ids[i];
  18298. delete nodes[id];
  18299. }
  18300. this._updateNodeIndexList();
  18301. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18302. this._resetLevels();
  18303. this._setupHierarchicalLayout();
  18304. }
  18305. this._updateCalculationNodes();
  18306. this._reconnectEdges();
  18307. this._updateSelection();
  18308. this._updateValueRange(nodes);
  18309. };
  18310. /**
  18311. * Load edges by reading the data table
  18312. * @param {Array | DataSet | DataView} edges The data containing the edges.
  18313. * @private
  18314. * @private
  18315. */
  18316. Network.prototype._setEdges = function(edges) {
  18317. var oldEdgesData = this.edgesData;
  18318. if (edges instanceof DataSet || edges instanceof DataView) {
  18319. this.edgesData = edges;
  18320. }
  18321. else if (edges instanceof Array) {
  18322. this.edgesData = new DataSet();
  18323. this.edgesData.add(edges);
  18324. }
  18325. else if (!edges) {
  18326. this.edgesData = new DataSet();
  18327. }
  18328. else {
  18329. throw new TypeError('Array or DataSet expected');
  18330. }
  18331. if (oldEdgesData) {
  18332. // unsubscribe from old dataset
  18333. util.forEach(this.edgesListeners, function (callback, event) {
  18334. oldEdgesData.off(event, callback);
  18335. });
  18336. }
  18337. // remove drawn edges
  18338. this.edges = {};
  18339. if (this.edgesData) {
  18340. // subscribe to new dataset
  18341. var me = this;
  18342. util.forEach(this.edgesListeners, function (callback, event) {
  18343. me.edgesData.on(event, callback);
  18344. });
  18345. // draw all new nodes
  18346. var ids = this.edgesData.getIds();
  18347. this._addEdges(ids);
  18348. }
  18349. this._reconnectEdges();
  18350. };
  18351. /**
  18352. * Add edges
  18353. * @param {Number[] | String[]} ids
  18354. * @private
  18355. */
  18356. Network.prototype._addEdges = function (ids) {
  18357. var edges = this.edges,
  18358. edgesData = this.edgesData;
  18359. for (var i = 0, len = ids.length; i < len; i++) {
  18360. var id = ids[i];
  18361. var oldEdge = edges[id];
  18362. if (oldEdge) {
  18363. oldEdge.disconnect();
  18364. }
  18365. var data = edgesData.get(id, {"showInternalIds" : true});
  18366. edges[id] = new Edge(data, this, this.constants);
  18367. }
  18368. this.moving = true;
  18369. this._updateValueRange(edges);
  18370. this._createBezierNodes();
  18371. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18372. this._resetLevels();
  18373. this._setupHierarchicalLayout();
  18374. }
  18375. this._updateCalculationNodes();
  18376. };
  18377. /**
  18378. * Update existing edges, or create them when not yet existing
  18379. * @param {Number[] | String[]} ids
  18380. * @private
  18381. */
  18382. Network.prototype._updateEdges = function (ids) {
  18383. var edges = this.edges,
  18384. edgesData = this.edgesData;
  18385. for (var i = 0, len = ids.length; i < len; i++) {
  18386. var id = ids[i];
  18387. var data = edgesData.get(id);
  18388. var edge = edges[id];
  18389. if (edge) {
  18390. // update edge
  18391. edge.disconnect();
  18392. edge.setProperties(data, this.constants);
  18393. edge.connect();
  18394. }
  18395. else {
  18396. // create edge
  18397. edge = new Edge(data, this, this.constants);
  18398. this.edges[id] = edge;
  18399. }
  18400. }
  18401. this._createBezierNodes();
  18402. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18403. this._resetLevels();
  18404. this._setupHierarchicalLayout();
  18405. }
  18406. this.moving = true;
  18407. this._updateValueRange(edges);
  18408. };
  18409. /**
  18410. * Remove existing edges. Non existing ids will be ignored
  18411. * @param {Number[] | String[]} ids
  18412. * @private
  18413. */
  18414. Network.prototype._removeEdges = function (ids) {
  18415. var edges = this.edges;
  18416. for (var i = 0, len = ids.length; i < len; i++) {
  18417. var id = ids[i];
  18418. var edge = edges[id];
  18419. if (edge) {
  18420. if (edge.via != null) {
  18421. delete this.sectors['support']['nodes'][edge.via.id];
  18422. }
  18423. edge.disconnect();
  18424. delete edges[id];
  18425. }
  18426. }
  18427. this.moving = true;
  18428. this._updateValueRange(edges);
  18429. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18430. this._resetLevels();
  18431. this._setupHierarchicalLayout();
  18432. }
  18433. this._updateCalculationNodes();
  18434. };
  18435. /**
  18436. * Reconnect all edges
  18437. * @private
  18438. */
  18439. Network.prototype._reconnectEdges = function() {
  18440. var id,
  18441. nodes = this.nodes,
  18442. edges = this.edges;
  18443. for (id in nodes) {
  18444. if (nodes.hasOwnProperty(id)) {
  18445. nodes[id].edges = [];
  18446. }
  18447. }
  18448. for (id in edges) {
  18449. if (edges.hasOwnProperty(id)) {
  18450. var edge = edges[id];
  18451. edge.from = null;
  18452. edge.to = null;
  18453. edge.connect();
  18454. }
  18455. }
  18456. };
  18457. /**
  18458. * Update the values of all object in the given array according to the current
  18459. * value range of the objects in the array.
  18460. * @param {Object} obj An object containing a set of Edges or Nodes
  18461. * The objects must have a method getValue() and
  18462. * setValueRange(min, max).
  18463. * @private
  18464. */
  18465. Network.prototype._updateValueRange = function(obj) {
  18466. var id;
  18467. // determine the range of the objects
  18468. var valueMin = undefined;
  18469. var valueMax = undefined;
  18470. for (id in obj) {
  18471. if (obj.hasOwnProperty(id)) {
  18472. var value = obj[id].getValue();
  18473. if (value !== undefined) {
  18474. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  18475. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  18476. }
  18477. }
  18478. }
  18479. // adjust the range of all objects
  18480. if (valueMin !== undefined && valueMax !== undefined) {
  18481. for (id in obj) {
  18482. if (obj.hasOwnProperty(id)) {
  18483. obj[id].setValueRange(valueMin, valueMax);
  18484. }
  18485. }
  18486. }
  18487. };
  18488. /**
  18489. * Redraw the network with the current data
  18490. * chart will be resized too.
  18491. */
  18492. Network.prototype.redraw = function() {
  18493. this.setSize(this.width, this.height);
  18494. this._redraw();
  18495. };
  18496. /**
  18497. * Redraw the network with the current data
  18498. * @private
  18499. */
  18500. Network.prototype._redraw = function() {
  18501. var ctx = this.frame.canvas.getContext('2d');
  18502. // clear the canvas
  18503. var w = this.frame.canvas.width;
  18504. var h = this.frame.canvas.height;
  18505. ctx.clearRect(0, 0, w, h);
  18506. // set scaling and translation
  18507. ctx.save();
  18508. ctx.translate(this.translation.x, this.translation.y);
  18509. ctx.scale(this.scale, this.scale);
  18510. this.canvasTopLeft = {
  18511. "x": this._XconvertDOMtoCanvas(0),
  18512. "y": this._YconvertDOMtoCanvas(0)
  18513. };
  18514. this.canvasBottomRight = {
  18515. "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
  18516. "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
  18517. };
  18518. this._doInAllSectors("_drawAllSectorNodes",ctx);
  18519. this._doInAllSectors("_drawEdges",ctx);
  18520. this._doInAllSectors("_drawNodes",ctx,false);
  18521. this._doInAllSectors("_drawControlNodes",ctx);
  18522. // this._doInSupportSector("_drawNodes",ctx,true);
  18523. // this._drawTree(ctx,"#F00F0F");
  18524. // restore original scaling and translation
  18525. ctx.restore();
  18526. };
  18527. /**
  18528. * Set the translation of the network
  18529. * @param {Number} offsetX Horizontal offset
  18530. * @param {Number} offsetY Vertical offset
  18531. * @private
  18532. */
  18533. Network.prototype._setTranslation = function(offsetX, offsetY) {
  18534. if (this.translation === undefined) {
  18535. this.translation = {
  18536. x: 0,
  18537. y: 0
  18538. };
  18539. }
  18540. if (offsetX !== undefined) {
  18541. this.translation.x = offsetX;
  18542. }
  18543. if (offsetY !== undefined) {
  18544. this.translation.y = offsetY;
  18545. }
  18546. this.emit('viewChanged');
  18547. };
  18548. /**
  18549. * Get the translation of the network
  18550. * @return {Object} translation An object with parameters x and y, both a number
  18551. * @private
  18552. */
  18553. Network.prototype._getTranslation = function() {
  18554. return {
  18555. x: this.translation.x,
  18556. y: this.translation.y
  18557. };
  18558. };
  18559. /**
  18560. * Scale the network
  18561. * @param {Number} scale Scaling factor 1.0 is unscaled
  18562. * @private
  18563. */
  18564. Network.prototype._setScale = function(scale) {
  18565. this.scale = scale;
  18566. };
  18567. /**
  18568. * Get the current scale of the network
  18569. * @return {Number} scale Scaling factor 1.0 is unscaled
  18570. * @private
  18571. */
  18572. Network.prototype._getScale = function() {
  18573. return this.scale;
  18574. };
  18575. /**
  18576. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  18577. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  18578. * @param {number} x
  18579. * @returns {number}
  18580. * @private
  18581. */
  18582. Network.prototype._XconvertDOMtoCanvas = function(x) {
  18583. return (x - this.translation.x) / this.scale;
  18584. };
  18585. /**
  18586. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  18587. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  18588. * @param {number} x
  18589. * @returns {number}
  18590. * @private
  18591. */
  18592. Network.prototype._XconvertCanvasToDOM = function(x) {
  18593. return x * this.scale + this.translation.x;
  18594. };
  18595. /**
  18596. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  18597. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  18598. * @param {number} y
  18599. * @returns {number}
  18600. * @private
  18601. */
  18602. Network.prototype._YconvertDOMtoCanvas = function(y) {
  18603. return (y - this.translation.y) / this.scale;
  18604. };
  18605. /**
  18606. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  18607. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  18608. * @param {number} y
  18609. * @returns {number}
  18610. * @private
  18611. */
  18612. Network.prototype._YconvertCanvasToDOM = function(y) {
  18613. return y * this.scale + this.translation.y ;
  18614. };
  18615. /**
  18616. *
  18617. * @param {object} pos = {x: number, y: number}
  18618. * @returns {{x: number, y: number}}
  18619. * @constructor
  18620. */
  18621. Network.prototype.canvasToDOM = function(pos) {
  18622. return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
  18623. }
  18624. /**
  18625. *
  18626. * @param {object} pos = {x: number, y: number}
  18627. * @returns {{x: number, y: number}}
  18628. * @constructor
  18629. */
  18630. Network.prototype.DOMtoCanvas = function(pos) {
  18631. return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
  18632. }
  18633. /**
  18634. * Redraw all nodes
  18635. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  18636. * @param {CanvasRenderingContext2D} ctx
  18637. * @param {Boolean} [alwaysShow]
  18638. * @private
  18639. */
  18640. Network.prototype._drawNodes = function(ctx,alwaysShow) {
  18641. if (alwaysShow === undefined) {
  18642. alwaysShow = false;
  18643. }
  18644. // first draw the unselected nodes
  18645. var nodes = this.nodes;
  18646. var selected = [];
  18647. for (var id in nodes) {
  18648. if (nodes.hasOwnProperty(id)) {
  18649. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  18650. if (nodes[id].isSelected()) {
  18651. selected.push(id);
  18652. }
  18653. else {
  18654. if (nodes[id].inArea() || alwaysShow) {
  18655. nodes[id].draw(ctx);
  18656. }
  18657. }
  18658. }
  18659. }
  18660. // draw the selected nodes on top
  18661. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  18662. if (nodes[selected[s]].inArea() || alwaysShow) {
  18663. nodes[selected[s]].draw(ctx);
  18664. }
  18665. }
  18666. };
  18667. /**
  18668. * Redraw all edges
  18669. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  18670. * @param {CanvasRenderingContext2D} ctx
  18671. * @private
  18672. */
  18673. Network.prototype._drawEdges = function(ctx) {
  18674. var edges = this.edges;
  18675. for (var id in edges) {
  18676. if (edges.hasOwnProperty(id)) {
  18677. var edge = edges[id];
  18678. edge.setScale(this.scale);
  18679. if (edge.connected) {
  18680. edges[id].draw(ctx);
  18681. }
  18682. }
  18683. }
  18684. };
  18685. /**
  18686. * Redraw all edges
  18687. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  18688. * @param {CanvasRenderingContext2D} ctx
  18689. * @private
  18690. */
  18691. Network.prototype._drawControlNodes = function(ctx) {
  18692. var edges = this.edges;
  18693. for (var id in edges) {
  18694. if (edges.hasOwnProperty(id)) {
  18695. edges[id]._drawControlNodes(ctx);
  18696. }
  18697. }
  18698. };
  18699. /**
  18700. * Find a stable position for all nodes
  18701. * @private
  18702. */
  18703. Network.prototype._stabilize = function() {
  18704. if (this.constants.freezeForStabilization == true) {
  18705. this._freezeDefinedNodes();
  18706. }
  18707. // find stable position
  18708. var count = 0;
  18709. while (this.moving && count < this.constants.stabilizationIterations) {
  18710. this._physicsTick();
  18711. count++;
  18712. }
  18713. this.zoomExtent(false,true);
  18714. if (this.constants.freezeForStabilization == true) {
  18715. this._restoreFrozenNodes();
  18716. }
  18717. this.emit("stabilized",{iterations:count});
  18718. };
  18719. /**
  18720. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  18721. * because only the supportnodes for the smoothCurves have to settle.
  18722. *
  18723. * @private
  18724. */
  18725. Network.prototype._freezeDefinedNodes = function() {
  18726. var nodes = this.nodes;
  18727. for (var id in nodes) {
  18728. if (nodes.hasOwnProperty(id)) {
  18729. if (nodes[id].x != null && nodes[id].y != null) {
  18730. nodes[id].fixedData.x = nodes[id].xFixed;
  18731. nodes[id].fixedData.y = nodes[id].yFixed;
  18732. nodes[id].xFixed = true;
  18733. nodes[id].yFixed = true;
  18734. }
  18735. }
  18736. }
  18737. };
  18738. /**
  18739. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  18740. *
  18741. * @private
  18742. */
  18743. Network.prototype._restoreFrozenNodes = function() {
  18744. var nodes = this.nodes;
  18745. for (var id in nodes) {
  18746. if (nodes.hasOwnProperty(id)) {
  18747. if (nodes[id].fixedData.x != null) {
  18748. nodes[id].xFixed = nodes[id].fixedData.x;
  18749. nodes[id].yFixed = nodes[id].fixedData.y;
  18750. }
  18751. }
  18752. }
  18753. };
  18754. /**
  18755. * Check if any of the nodes is still moving
  18756. * @param {number} vmin the minimum velocity considered as 'moving'
  18757. * @return {boolean} true if moving, false if non of the nodes is moving
  18758. * @private
  18759. */
  18760. Network.prototype._isMoving = function(vmin) {
  18761. var nodes = this.nodes;
  18762. for (var id in nodes) {
  18763. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  18764. return true;
  18765. }
  18766. }
  18767. return false;
  18768. };
  18769. /**
  18770. * /**
  18771. * Perform one discrete step for all nodes
  18772. *
  18773. * @private
  18774. */
  18775. Network.prototype._discreteStepNodes = function() {
  18776. var interval = this.physicsDiscreteStepsize;
  18777. var nodes = this.nodes;
  18778. var nodeId;
  18779. var nodesPresent = false;
  18780. if (this.constants.maxVelocity > 0) {
  18781. for (nodeId in nodes) {
  18782. if (nodes.hasOwnProperty(nodeId)) {
  18783. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  18784. nodesPresent = true;
  18785. }
  18786. }
  18787. }
  18788. else {
  18789. for (nodeId in nodes) {
  18790. if (nodes.hasOwnProperty(nodeId)) {
  18791. nodes[nodeId].discreteStep(interval);
  18792. nodesPresent = true;
  18793. }
  18794. }
  18795. }
  18796. if (nodesPresent == true) {
  18797. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  18798. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  18799. this.moving = true;
  18800. }
  18801. else {
  18802. this.moving = this._isMoving(vminCorrected);
  18803. }
  18804. }
  18805. };
  18806. /**
  18807. * A single simulation step (or "tick") in the physics simulation
  18808. *
  18809. * @private
  18810. */
  18811. Network.prototype._physicsTick = function() {
  18812. if (!this.freezeSimulation) {
  18813. if (this.moving) {
  18814. this._doInAllActiveSectors("_initializeForceCalculation");
  18815. this._doInAllActiveSectors("_discreteStepNodes");
  18816. if (this.constants.smoothCurves) {
  18817. this._doInSupportSector("_discreteStepNodes");
  18818. }
  18819. this._findCenter(this._getRange())
  18820. }
  18821. }
  18822. };
  18823. /**
  18824. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  18825. * It reschedules itself at the beginning of the function
  18826. *
  18827. * @private
  18828. */
  18829. Network.prototype._animationStep = function() {
  18830. // reset the timer so a new scheduled animation step can be set
  18831. this.timer = undefined;
  18832. // handle the keyboad movement
  18833. this._handleNavigation();
  18834. // this schedules a new animation step
  18835. this.start();
  18836. // start the physics simulation
  18837. var calculationTime = Date.now();
  18838. var maxSteps = 1;
  18839. this._physicsTick();
  18840. var timeRequired = Date.now() - calculationTime;
  18841. while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  18842. this._physicsTick();
  18843. timeRequired = Date.now() - calculationTime;
  18844. maxSteps++;
  18845. }
  18846. // start the rendering process
  18847. var renderTime = Date.now();
  18848. this._redraw();
  18849. this.renderTime = Date.now() - renderTime;
  18850. };
  18851. if (typeof window !== 'undefined') {
  18852. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  18853. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  18854. }
  18855. /**
  18856. * Schedule a animation step with the refreshrate interval.
  18857. */
  18858. Network.prototype.start = function() {
  18859. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  18860. if (!this.timer) {
  18861. var ua = navigator.userAgent.toLowerCase();
  18862. var requiresTimeout = false;
  18863. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  18864. requiresTimeout = true;
  18865. }
  18866. else if (ua.indexOf('safari') != -1) { // safari
  18867. if (ua.indexOf('chrome') <= -1) {
  18868. requiresTimeout = true;
  18869. }
  18870. }
  18871. if (requiresTimeout == true) {
  18872. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  18873. }
  18874. else{
  18875. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  18876. }
  18877. }
  18878. }
  18879. else {
  18880. this._redraw();
  18881. }
  18882. };
  18883. /**
  18884. * Move the network according to the keyboard presses.
  18885. *
  18886. * @private
  18887. */
  18888. Network.prototype._handleNavigation = function() {
  18889. if (this.xIncrement != 0 || this.yIncrement != 0) {
  18890. var translation = this._getTranslation();
  18891. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  18892. }
  18893. if (this.zoomIncrement != 0) {
  18894. var center = {
  18895. x: this.frame.canvas.clientWidth / 2,
  18896. y: this.frame.canvas.clientHeight / 2
  18897. };
  18898. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  18899. }
  18900. };
  18901. /**
  18902. * Freeze the _animationStep
  18903. */
  18904. Network.prototype.toggleFreeze = function() {
  18905. if (this.freezeSimulation == false) {
  18906. this.freezeSimulation = true;
  18907. }
  18908. else {
  18909. this.freezeSimulation = false;
  18910. this.start();
  18911. }
  18912. };
  18913. /**
  18914. * This function cleans the support nodes if they are not needed and adds them when they are.
  18915. *
  18916. * @param {boolean} [disableStart]
  18917. * @private
  18918. */
  18919. Network.prototype._configureSmoothCurves = function(disableStart) {
  18920. if (disableStart === undefined) {
  18921. disableStart = true;
  18922. }
  18923. if (this.constants.smoothCurves == true) {
  18924. this._createBezierNodes();
  18925. }
  18926. else {
  18927. // delete the support nodes
  18928. this.sectors['support']['nodes'] = {};
  18929. for (var edgeId in this.edges) {
  18930. if (this.edges.hasOwnProperty(edgeId)) {
  18931. this.edges[edgeId].smooth = false;
  18932. this.edges[edgeId].via = null;
  18933. }
  18934. }
  18935. }
  18936. this._updateCalculationNodes();
  18937. if (!disableStart) {
  18938. this.moving = true;
  18939. this.start();
  18940. }
  18941. };
  18942. /**
  18943. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  18944. * are used for the force calculation.
  18945. *
  18946. * @private
  18947. */
  18948. Network.prototype._createBezierNodes = function() {
  18949. if (this.constants.smoothCurves == true) {
  18950. for (var edgeId in this.edges) {
  18951. if (this.edges.hasOwnProperty(edgeId)) {
  18952. var edge = this.edges[edgeId];
  18953. if (edge.via == null) {
  18954. edge.smooth = true;
  18955. var nodeId = "edgeId:".concat(edge.id);
  18956. this.sectors['support']['nodes'][nodeId] = new Node(
  18957. {id:nodeId,
  18958. mass:1,
  18959. shape:'circle',
  18960. image:"",
  18961. internalMultiplier:1
  18962. },{},{},this.constants);
  18963. edge.via = this.sectors['support']['nodes'][nodeId];
  18964. edge.via.parentEdgeId = edge.id;
  18965. edge.positionBezierNode();
  18966. }
  18967. }
  18968. }
  18969. }
  18970. };
  18971. /**
  18972. * load the functions that load the mixins into the prototype.
  18973. *
  18974. * @private
  18975. */
  18976. Network.prototype._initializeMixinLoaders = function () {
  18977. for (var mixinFunction in networkMixinLoaders) {
  18978. if (networkMixinLoaders.hasOwnProperty(mixinFunction)) {
  18979. Network.prototype[mixinFunction] = networkMixinLoaders[mixinFunction];
  18980. }
  18981. }
  18982. };
  18983. /**
  18984. * Load the XY positions of the nodes into the dataset.
  18985. */
  18986. Network.prototype.storePosition = function() {
  18987. var dataArray = [];
  18988. for (var nodeId in this.nodes) {
  18989. if (this.nodes.hasOwnProperty(nodeId)) {
  18990. var node = this.nodes[nodeId];
  18991. var allowedToMoveX = !this.nodes.xFixed;
  18992. var allowedToMoveY = !this.nodes.yFixed;
  18993. if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) {
  18994. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  18995. }
  18996. }
  18997. }
  18998. this.nodesData.update(dataArray);
  18999. };
  19000. /**
  19001. * Center a node in view.
  19002. *
  19003. * @param {Number} nodeId
  19004. * @param {Number} [zoomLevel]
  19005. */
  19006. Network.prototype.focusOnNode = function (nodeId, zoomLevel) {
  19007. if (this.nodes.hasOwnProperty(nodeId)) {
  19008. if (zoomLevel === undefined) {
  19009. zoomLevel = this._getScale();
  19010. }
  19011. var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
  19012. var requiredScale = zoomLevel;
  19013. this._setScale(requiredScale);
  19014. var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height});
  19015. var translation = this._getTranslation();
  19016. var distanceFromCenter = {x:canvasCenter.x - nodePosition.x,
  19017. y:canvasCenter.y - nodePosition.y};
  19018. this._setTranslation(translation.x + requiredScale * distanceFromCenter.x,
  19019. translation.y + requiredScale * distanceFromCenter.y);
  19020. this.redraw();
  19021. }
  19022. else {
  19023. console.log("This nodeId cannot be found.")
  19024. }
  19025. };
  19026. /**
  19027. * @constructor Graph3d
  19028. * Graph3d displays data in 3d.
  19029. *
  19030. * Graph3d is developed in javascript as a Google Visualization Chart.
  19031. *
  19032. * @param {Element} container The DOM element in which the Graph3d will
  19033. * be created. Normally a div element.
  19034. * @param {DataSet | DataView | Array} [data]
  19035. * @param {Object} [options]
  19036. */
  19037. function Graph3d(container, data, options) {
  19038. if (!(this instanceof Graph3d)) {
  19039. throw new SyntaxError('Constructor must be called with the new operator');
  19040. }
  19041. // create variables and set default values
  19042. this.containerElement = container;
  19043. this.width = '400px';
  19044. this.height = '400px';
  19045. this.margin = 10; // px
  19046. this.defaultXCenter = '55%';
  19047. this.defaultYCenter = '50%';
  19048. this.xLabel = 'x';
  19049. this.yLabel = 'y';
  19050. this.zLabel = 'z';
  19051. this.filterLabel = 'time';
  19052. this.legendLabel = 'value';
  19053. this.style = Graph3d.STYLE.DOT;
  19054. this.showPerspective = true;
  19055. this.showGrid = true;
  19056. this.keepAspectRatio = true;
  19057. this.showShadow = false;
  19058. this.showGrayBottom = false; // TODO: this does not work correctly
  19059. this.showTooltip = false;
  19060. this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
  19061. this.animationInterval = 1000; // milliseconds
  19062. this.animationPreload = false;
  19063. this.camera = new Graph3d.Camera();
  19064. this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
  19065. this.dataTable = null; // The original data table
  19066. this.dataPoints = null; // The table with point objects
  19067. // the column indexes
  19068. this.colX = undefined;
  19069. this.colY = undefined;
  19070. this.colZ = undefined;
  19071. this.colValue = undefined;
  19072. this.colFilter = undefined;
  19073. this.xMin = 0;
  19074. this.xStep = undefined; // auto by default
  19075. this.xMax = 1;
  19076. this.yMin = 0;
  19077. this.yStep = undefined; // auto by default
  19078. this.yMax = 1;
  19079. this.zMin = 0;
  19080. this.zStep = undefined; // auto by default
  19081. this.zMax = 1;
  19082. this.valueMin = 0;
  19083. this.valueMax = 1;
  19084. this.xBarWidth = 1;
  19085. this.yBarWidth = 1;
  19086. // TODO: customize axis range
  19087. // constants
  19088. this.colorAxis = '#4D4D4D';
  19089. this.colorGrid = '#D3D3D3';
  19090. this.colorDot = '#7DC1FF';
  19091. this.colorDotBorder = '#3267D2';
  19092. // create a frame and canvas
  19093. this.create();
  19094. // apply options (also when undefined)
  19095. this.setOptions(options);
  19096. // apply data
  19097. if (data) {
  19098. this.setData(data);
  19099. }
  19100. }
  19101. // Extend Graph3d with an Emitter mixin
  19102. Emitter(Graph3d.prototype);
  19103. /**
  19104. * @class Camera
  19105. * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
  19106. * The camera is always looking in the direction of the origin of the arm.
  19107. * This way, the camera always rotates around one fixed point, the location
  19108. * of the camera arm.
  19109. *
  19110. * Documentation:
  19111. * http://en.wikipedia.org/wiki/3D_projection
  19112. */
  19113. Graph3d.Camera = function () {
  19114. this.armLocation = new Point3d();
  19115. this.armRotation = {};
  19116. this.armRotation.horizontal = 0;
  19117. this.armRotation.vertical = 0;
  19118. this.armLength = 1.7;
  19119. this.cameraLocation = new Point3d();
  19120. this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
  19121. this.calculateCameraOrientation();
  19122. };
  19123. /**
  19124. * Set the location (origin) of the arm
  19125. * @param {Number} x Normalized value of x
  19126. * @param {Number} y Normalized value of y
  19127. * @param {Number} z Normalized value of z
  19128. */
  19129. Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
  19130. this.armLocation.x = x;
  19131. this.armLocation.y = y;
  19132. this.armLocation.z = z;
  19133. this.calculateCameraOrientation();
  19134. };
  19135. /**
  19136. * Set the rotation of the camera arm
  19137. * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
  19138. * Optional, can be left undefined.
  19139. * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
  19140. * if vertical=0.5*PI, the graph is shown from the
  19141. * top. Optional, can be left undefined.
  19142. */
  19143. Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
  19144. if (horizontal !== undefined) {
  19145. this.armRotation.horizontal = horizontal;
  19146. }
  19147. if (vertical !== undefined) {
  19148. this.armRotation.vertical = vertical;
  19149. if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
  19150. if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
  19151. }
  19152. if (horizontal !== undefined || vertical !== undefined) {
  19153. this.calculateCameraOrientation();
  19154. }
  19155. };
  19156. /**
  19157. * Retrieve the current arm rotation
  19158. * @return {object} An object with parameters horizontal and vertical
  19159. */
  19160. Graph3d.Camera.prototype.getArmRotation = function() {
  19161. var rot = {};
  19162. rot.horizontal = this.armRotation.horizontal;
  19163. rot.vertical = this.armRotation.vertical;
  19164. return rot;
  19165. };
  19166. /**
  19167. * Set the (normalized) length of the camera arm.
  19168. * @param {Number} length A length between 0.71 and 5.0
  19169. */
  19170. Graph3d.Camera.prototype.setArmLength = function(length) {
  19171. if (length === undefined)
  19172. return;
  19173. this.armLength = length;
  19174. // Radius must be larger than the corner of the graph,
  19175. // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
  19176. // graph
  19177. if (this.armLength < 0.71) this.armLength = 0.71;
  19178. if (this.armLength > 5.0) this.armLength = 5.0;
  19179. this.calculateCameraOrientation();
  19180. };
  19181. /**
  19182. * Retrieve the arm length
  19183. * @return {Number} length
  19184. */
  19185. Graph3d.Camera.prototype.getArmLength = function() {
  19186. return this.armLength;
  19187. };
  19188. /**
  19189. * Retrieve the camera location
  19190. * @return {Point3d} cameraLocation
  19191. */
  19192. Graph3d.Camera.prototype.getCameraLocation = function() {
  19193. return this.cameraLocation;
  19194. };
  19195. /**
  19196. * Retrieve the camera rotation
  19197. * @return {Point3d} cameraRotation
  19198. */
  19199. Graph3d.Camera.prototype.getCameraRotation = function() {
  19200. return this.cameraRotation;
  19201. };
  19202. /**
  19203. * Calculate the location and rotation of the camera based on the
  19204. * position and orientation of the camera arm
  19205. */
  19206. Graph3d.Camera.prototype.calculateCameraOrientation = function() {
  19207. // calculate location of the camera
  19208. this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  19209. this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  19210. this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
  19211. // calculate rotation of the camera
  19212. this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
  19213. this.cameraRotation.y = 0;
  19214. this.cameraRotation.z = -this.armRotation.horizontal;
  19215. };
  19216. /**
  19217. * Calculate the scaling values, dependent on the range in x, y, and z direction
  19218. */
  19219. Graph3d.prototype._setScale = function() {
  19220. this.scale = new Point3d(1 / (this.xMax - this.xMin),
  19221. 1 / (this.yMax - this.yMin),
  19222. 1 / (this.zMax - this.zMin));
  19223. // keep aspect ration between x and y scale if desired
  19224. if (this.keepAspectRatio) {
  19225. if (this.scale.x < this.scale.y) {
  19226. //noinspection JSSuspiciousNameCombination
  19227. this.scale.y = this.scale.x;
  19228. }
  19229. else {
  19230. //noinspection JSSuspiciousNameCombination
  19231. this.scale.x = this.scale.y;
  19232. }
  19233. }
  19234. // scale the vertical axis
  19235. this.scale.z *= this.verticalRatio;
  19236. // TODO: can this be automated? verticalRatio?
  19237. // determine scale for (optional) value
  19238. this.scale.value = 1 / (this.valueMax - this.valueMin);
  19239. // position the camera arm
  19240. var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
  19241. var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
  19242. var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
  19243. this.camera.setArmLocation(xCenter, yCenter, zCenter);
  19244. };
  19245. /**
  19246. * Convert a 3D location to a 2D location on screen
  19247. * http://en.wikipedia.org/wiki/3D_projection
  19248. * @param {Point3d} point3d A 3D point with parameters x, y, z
  19249. * @return {Point2d} point2d A 2D point with parameters x, y
  19250. */
  19251. Graph3d.prototype._convert3Dto2D = function(point3d) {
  19252. var translation = this._convertPointToTranslation(point3d);
  19253. return this._convertTranslationToScreen(translation);
  19254. };
  19255. /**
  19256. * Convert a 3D location its translation seen from the camera
  19257. * http://en.wikipedia.org/wiki/3D_projection
  19258. * @param {Point3d} point3d A 3D point with parameters x, y, z
  19259. * @return {Point3d} translation A 3D point with parameters x, y, z This is
  19260. * the translation of the point, seen from the
  19261. * camera
  19262. */
  19263. Graph3d.prototype._convertPointToTranslation = function(point3d) {
  19264. var ax = point3d.x * this.scale.x,
  19265. ay = point3d.y * this.scale.y,
  19266. az = point3d.z * this.scale.z,
  19267. cx = this.camera.getCameraLocation().x,
  19268. cy = this.camera.getCameraLocation().y,
  19269. cz = this.camera.getCameraLocation().z,
  19270. // calculate angles
  19271. sinTx = Math.sin(this.camera.getCameraRotation().x),
  19272. cosTx = Math.cos(this.camera.getCameraRotation().x),
  19273. sinTy = Math.sin(this.camera.getCameraRotation().y),
  19274. cosTy = Math.cos(this.camera.getCameraRotation().y),
  19275. sinTz = Math.sin(this.camera.getCameraRotation().z),
  19276. cosTz = Math.cos(this.camera.getCameraRotation().z),
  19277. // calculate translation
  19278. dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
  19279. dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
  19280. dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
  19281. return new Point3d(dx, dy, dz);
  19282. };
  19283. /**
  19284. * Convert a translation point to a point on the screen
  19285. * @param {Point3d} translation A 3D point with parameters x, y, z This is
  19286. * the translation of the point, seen from the
  19287. * camera
  19288. * @return {Point2d} point2d A 2D point with parameters x, y
  19289. */
  19290. Graph3d.prototype._convertTranslationToScreen = function(translation) {
  19291. var ex = this.eye.x,
  19292. ey = this.eye.y,
  19293. ez = this.eye.z,
  19294. dx = translation.x,
  19295. dy = translation.y,
  19296. dz = translation.z;
  19297. // calculate position on screen from translation
  19298. var bx;
  19299. var by;
  19300. if (this.showPerspective) {
  19301. bx = (dx - ex) * (ez / dz);
  19302. by = (dy - ey) * (ez / dz);
  19303. }
  19304. else {
  19305. bx = dx * -(ez / this.camera.getArmLength());
  19306. by = dy * -(ez / this.camera.getArmLength());
  19307. }
  19308. // shift and scale the point to the center of the screen
  19309. // use the width of the graph to scale both horizontally and vertically.
  19310. return new Point2d(
  19311. this.xcenter + bx * this.frame.canvas.clientWidth,
  19312. this.ycenter - by * this.frame.canvas.clientWidth);
  19313. };
  19314. /**
  19315. * Set the background styling for the graph
  19316. * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
  19317. */
  19318. Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
  19319. var fill = 'white';
  19320. var stroke = 'gray';
  19321. var strokeWidth = 1;
  19322. if (typeof(backgroundColor) === 'string') {
  19323. fill = backgroundColor;
  19324. stroke = 'none';
  19325. strokeWidth = 0;
  19326. }
  19327. else if (typeof(backgroundColor) === 'object') {
  19328. if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
  19329. if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
  19330. if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
  19331. }
  19332. else if (backgroundColor === undefined) {
  19333. // use use defaults
  19334. }
  19335. else {
  19336. throw 'Unsupported type of backgroundColor';
  19337. }
  19338. this.frame.style.backgroundColor = fill;
  19339. this.frame.style.borderColor = stroke;
  19340. this.frame.style.borderWidth = strokeWidth + 'px';
  19341. this.frame.style.borderStyle = 'solid';
  19342. };
  19343. /// enumerate the available styles
  19344. Graph3d.STYLE = {
  19345. BAR: 0,
  19346. BARCOLOR: 1,
  19347. BARSIZE: 2,
  19348. DOT : 3,
  19349. DOTLINE : 4,
  19350. DOTCOLOR: 5,
  19351. DOTSIZE: 6,
  19352. GRID : 7,
  19353. LINE: 8,
  19354. SURFACE : 9
  19355. };
  19356. /**
  19357. * Retrieve the style index from given styleName
  19358. * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
  19359. * @return {Number} styleNumber Enumeration value representing the style, or -1
  19360. * when not found
  19361. */
  19362. Graph3d.prototype._getStyleNumber = function(styleName) {
  19363. switch (styleName) {
  19364. case 'dot': return Graph3d.STYLE.DOT;
  19365. case 'dot-line': return Graph3d.STYLE.DOTLINE;
  19366. case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
  19367. case 'dot-size': return Graph3d.STYLE.DOTSIZE;
  19368. case 'line': return Graph3d.STYLE.LINE;
  19369. case 'grid': return Graph3d.STYLE.GRID;
  19370. case 'surface': return Graph3d.STYLE.SURFACE;
  19371. case 'bar': return Graph3d.STYLE.BAR;
  19372. case 'bar-color': return Graph3d.STYLE.BARCOLOR;
  19373. case 'bar-size': return Graph3d.STYLE.BARSIZE;
  19374. }
  19375. return -1;
  19376. };
  19377. /**
  19378. * Determine the indexes of the data columns, based on the given style and data
  19379. * @param {DataSet} data
  19380. * @param {Number} style
  19381. */
  19382. Graph3d.prototype._determineColumnIndexes = function(data, style) {
  19383. if (this.style === Graph3d.STYLE.DOT ||
  19384. this.style === Graph3d.STYLE.DOTLINE ||
  19385. this.style === Graph3d.STYLE.LINE ||
  19386. this.style === Graph3d.STYLE.GRID ||
  19387. this.style === Graph3d.STYLE.SURFACE ||
  19388. this.style === Graph3d.STYLE.BAR) {
  19389. // 3 columns expected, and optionally a 4th with filter values
  19390. this.colX = 0;
  19391. this.colY = 1;
  19392. this.colZ = 2;
  19393. this.colValue = undefined;
  19394. if (data.getNumberOfColumns() > 3) {
  19395. this.colFilter = 3;
  19396. }
  19397. }
  19398. else if (this.style === Graph3d.STYLE.DOTCOLOR ||
  19399. this.style === Graph3d.STYLE.DOTSIZE ||
  19400. this.style === Graph3d.STYLE.BARCOLOR ||
  19401. this.style === Graph3d.STYLE.BARSIZE) {
  19402. // 4 columns expected, and optionally a 5th with filter values
  19403. this.colX = 0;
  19404. this.colY = 1;
  19405. this.colZ = 2;
  19406. this.colValue = 3;
  19407. if (data.getNumberOfColumns() > 4) {
  19408. this.colFilter = 4;
  19409. }
  19410. }
  19411. else {
  19412. throw 'Unknown style "' + this.style + '"';
  19413. }
  19414. };
  19415. Graph3d.prototype.getNumberOfRows = function(data) {
  19416. return data.length;
  19417. }
  19418. Graph3d.prototype.getNumberOfColumns = function(data) {
  19419. var counter = 0;
  19420. for (var column in data[0]) {
  19421. if (data[0].hasOwnProperty(column)) {
  19422. counter++;
  19423. }
  19424. }
  19425. return counter;
  19426. }
  19427. Graph3d.prototype.getDistinctValues = function(data, column) {
  19428. var distinctValues = [];
  19429. for (var i = 0; i < data.length; i++) {
  19430. if (distinctValues.indexOf(data[i][column]) == -1) {
  19431. distinctValues.push(data[i][column]);
  19432. }
  19433. }
  19434. return distinctValues;
  19435. }
  19436. Graph3d.prototype.getColumnRange = function(data,column) {
  19437. var minMax = {min:data[0][column],max:data[0][column]};
  19438. for (var i = 0; i < data.length; i++) {
  19439. if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
  19440. if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
  19441. }
  19442. return minMax;
  19443. };
  19444. /**
  19445. * Initialize the data from the data table. Calculate minimum and maximum values
  19446. * and column index values
  19447. * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
  19448. * @param {Number} style Style Number
  19449. */
  19450. Graph3d.prototype._dataInitialize = function (rawData, style) {
  19451. var me = this;
  19452. // unsubscribe from the dataTable
  19453. if (this.dataSet) {
  19454. this.dataSet.off('*', this._onChange);
  19455. }
  19456. if (rawData === undefined)
  19457. return;
  19458. if (Array.isArray(rawData)) {
  19459. rawData = new DataSet(rawData);
  19460. }
  19461. var data;
  19462. if (rawData instanceof DataSet || rawData instanceof DataView) {
  19463. data = rawData.get();
  19464. }
  19465. else {
  19466. throw new Error('Array, DataSet, or DataView expected');
  19467. }
  19468. if (data.length == 0)
  19469. return;
  19470. this.dataSet = rawData;
  19471. this.dataTable = data;
  19472. // subscribe to changes in the dataset
  19473. this._onChange = function () {
  19474. me.setData(me.dataSet);
  19475. };
  19476. this.dataSet.on('*', this._onChange);
  19477. // _determineColumnIndexes
  19478. // getNumberOfRows (points)
  19479. // getNumberOfColumns (x,y,z,v,t,t1,t2...)
  19480. // getDistinctValues (unique values?)
  19481. // getColumnRange
  19482. // determine the location of x,y,z,value,filter columns
  19483. this.colX = 'x';
  19484. this.colY = 'y';
  19485. this.colZ = 'z';
  19486. this.colValue = 'style';
  19487. this.colFilter = 'filter';
  19488. // check if a filter column is provided
  19489. if (data[0].hasOwnProperty('filter')) {
  19490. if (this.dataFilter === undefined) {
  19491. this.dataFilter = new Filter(rawData, this.colFilter, this);
  19492. this.dataFilter.setOnLoadCallback(function() {me.redraw();});
  19493. }
  19494. }
  19495. var withBars = this.style == Graph3d.STYLE.BAR ||
  19496. this.style == Graph3d.STYLE.BARCOLOR ||
  19497. this.style == Graph3d.STYLE.BARSIZE;
  19498. // determine barWidth from data
  19499. if (withBars) {
  19500. if (this.defaultXBarWidth !== undefined) {
  19501. this.xBarWidth = this.defaultXBarWidth;
  19502. }
  19503. else {
  19504. var dataX = this.getDistinctValues(data,this.colX);
  19505. this.xBarWidth = (dataX[1] - dataX[0]) || 1;
  19506. }
  19507. if (this.defaultYBarWidth !== undefined) {
  19508. this.yBarWidth = this.defaultYBarWidth;
  19509. }
  19510. else {
  19511. var dataY = this.getDistinctValues(data,this.colY);
  19512. this.yBarWidth = (dataY[1] - dataY[0]) || 1;
  19513. }
  19514. }
  19515. // calculate minimums and maximums
  19516. var xRange = this.getColumnRange(data,this.colX);
  19517. if (withBars) {
  19518. xRange.min -= this.xBarWidth / 2;
  19519. xRange.max += this.xBarWidth / 2;
  19520. }
  19521. this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
  19522. this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
  19523. if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
  19524. this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
  19525. var yRange = this.getColumnRange(data,this.colY);
  19526. if (withBars) {
  19527. yRange.min -= this.yBarWidth / 2;
  19528. yRange.max += this.yBarWidth / 2;
  19529. }
  19530. this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
  19531. this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
  19532. if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
  19533. this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
  19534. var zRange = this.getColumnRange(data,this.colZ);
  19535. this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
  19536. this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
  19537. if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
  19538. this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
  19539. if (this.colValue !== undefined) {
  19540. var valueRange = this.getColumnRange(data,this.colValue);
  19541. this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
  19542. this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
  19543. if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
  19544. }
  19545. // set the scale dependent on the ranges.
  19546. this._setScale();
  19547. };
  19548. /**
  19549. * Filter the data based on the current filter
  19550. * @param {Array} data
  19551. * @return {Array} dataPoints Array with point objects which can be drawn on screen
  19552. */
  19553. Graph3d.prototype._getDataPoints = function (data) {
  19554. // TODO: store the created matrix dataPoints in the filters instead of reloading each time
  19555. var x, y, i, z, obj, point;
  19556. var dataPoints = [];
  19557. if (this.style === Graph3d.STYLE.GRID ||
  19558. this.style === Graph3d.STYLE.SURFACE) {
  19559. // copy all values from the google data table to a matrix
  19560. // the provided values are supposed to form a grid of (x,y) positions
  19561. // create two lists with all present x and y values
  19562. var dataX = [];
  19563. var dataY = [];
  19564. for (i = 0; i < this.getNumberOfRows(data); i++) {
  19565. x = data[i][this.colX] || 0;
  19566. y = data[i][this.colY] || 0;
  19567. if (dataX.indexOf(x) === -1) {
  19568. dataX.push(x);
  19569. }
  19570. if (dataY.indexOf(y) === -1) {
  19571. dataY.push(y);
  19572. }
  19573. }
  19574. function sortNumber(a, b) {
  19575. return a - b;
  19576. }
  19577. dataX.sort(sortNumber);
  19578. dataY.sort(sortNumber);
  19579. // create a grid, a 2d matrix, with all values.
  19580. var dataMatrix = []; // temporary data matrix
  19581. for (i = 0; i < data.length; i++) {
  19582. x = data[i][this.colX] || 0;
  19583. y = data[i][this.colY] || 0;
  19584. z = data[i][this.colZ] || 0;
  19585. var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
  19586. var yIndex = dataY.indexOf(y);
  19587. if (dataMatrix[xIndex] === undefined) {
  19588. dataMatrix[xIndex] = [];
  19589. }
  19590. var point3d = new Point3d();
  19591. point3d.x = x;
  19592. point3d.y = y;
  19593. point3d.z = z;
  19594. obj = {};
  19595. obj.point = point3d;
  19596. obj.trans = undefined;
  19597. obj.screen = undefined;
  19598. obj.bottom = new Point3d(x, y, this.zMin);
  19599. dataMatrix[xIndex][yIndex] = obj;
  19600. dataPoints.push(obj);
  19601. }
  19602. // fill in the pointers to the neighbors.
  19603. for (x = 0; x < dataMatrix.length; x++) {
  19604. for (y = 0; y < dataMatrix[x].length; y++) {
  19605. if (dataMatrix[x][y]) {
  19606. dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
  19607. dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
  19608. dataMatrix[x][y].pointCross =
  19609. (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
  19610. dataMatrix[x+1][y+1] :
  19611. undefined;
  19612. }
  19613. }
  19614. }
  19615. }
  19616. else { // 'dot', 'dot-line', etc.
  19617. // copy all values from the google data table to a list with Point3d objects
  19618. for (i = 0; i < data.length; i++) {
  19619. point = new Point3d();
  19620. point.x = data[i][this.colX] || 0;
  19621. point.y = data[i][this.colY] || 0;
  19622. point.z = data[i][this.colZ] || 0;
  19623. if (this.colValue !== undefined) {
  19624. point.value = data[i][this.colValue] || 0;
  19625. }
  19626. obj = {};
  19627. obj.point = point;
  19628. obj.bottom = new Point3d(point.x, point.y, this.zMin);
  19629. obj.trans = undefined;
  19630. obj.screen = undefined;
  19631. dataPoints.push(obj);
  19632. }
  19633. }
  19634. return dataPoints;
  19635. };
  19636. /**
  19637. * Append suffix 'px' to provided value x
  19638. * @param {int} x An integer value
  19639. * @return {string} the string value of x, followed by the suffix 'px'
  19640. */
  19641. Graph3d.px = function(x) {
  19642. return x + 'px';
  19643. };
  19644. /**
  19645. * Create the main frame for the Graph3d.
  19646. * This function is executed once when a Graph3d object is created. The frame
  19647. * contains a canvas, and this canvas contains all objects like the axis and
  19648. * nodes.
  19649. */
  19650. Graph3d.prototype.create = function () {
  19651. // remove all elements from the container element.
  19652. while (this.containerElement.hasChildNodes()) {
  19653. this.containerElement.removeChild(this.containerElement.firstChild);
  19654. }
  19655. this.frame = document.createElement('div');
  19656. this.frame.style.position = 'relative';
  19657. this.frame.style.overflow = 'hidden';
  19658. // create the graph canvas (HTML canvas element)
  19659. this.frame.canvas = document.createElement( 'canvas' );
  19660. this.frame.canvas.style.position = 'relative';
  19661. this.frame.appendChild(this.frame.canvas);
  19662. //if (!this.frame.canvas.getContext) {
  19663. {
  19664. var noCanvas = document.createElement( 'DIV' );
  19665. noCanvas.style.color = 'red';
  19666. noCanvas.style.fontWeight = 'bold' ;
  19667. noCanvas.style.padding = '10px';
  19668. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  19669. this.frame.canvas.appendChild(noCanvas);
  19670. }
  19671. this.frame.filter = document.createElement( 'div' );
  19672. this.frame.filter.style.position = 'absolute';
  19673. this.frame.filter.style.bottom = '0px';
  19674. this.frame.filter.style.left = '0px';
  19675. this.frame.filter.style.width = '100%';
  19676. this.frame.appendChild(this.frame.filter);
  19677. // add event listeners to handle moving and zooming the contents
  19678. var me = this;
  19679. var onmousedown = function (event) {me._onMouseDown(event);};
  19680. var ontouchstart = function (event) {me._onTouchStart(event);};
  19681. var onmousewheel = function (event) {me._onWheel(event);};
  19682. var ontooltip = function (event) {me._onTooltip(event);};
  19683. // TODO: these events are never cleaned up... can give a 'memory leakage'
  19684. G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
  19685. G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
  19686. G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
  19687. G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
  19688. G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
  19689. // add the new graph to the container element
  19690. this.containerElement.appendChild(this.frame);
  19691. };
  19692. /**
  19693. * Set a new size for the graph
  19694. * @param {string} width Width in pixels or percentage (for example '800px'
  19695. * or '50%')
  19696. * @param {string} height Height in pixels or percentage (for example '400px'
  19697. * or '30%')
  19698. */
  19699. Graph3d.prototype.setSize = function(width, height) {
  19700. this.frame.style.width = width;
  19701. this.frame.style.height = height;
  19702. this._resizeCanvas();
  19703. };
  19704. /**
  19705. * Resize the canvas to the current size of the frame
  19706. */
  19707. Graph3d.prototype._resizeCanvas = function() {
  19708. this.frame.canvas.style.width = '100%';
  19709. this.frame.canvas.style.height = '100%';
  19710. this.frame.canvas.width = this.frame.canvas.clientWidth;
  19711. this.frame.canvas.height = this.frame.canvas.clientHeight;
  19712. // adjust with for margin
  19713. this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
  19714. };
  19715. /**
  19716. * Start animation
  19717. */
  19718. Graph3d.prototype.animationStart = function() {
  19719. if (!this.frame.filter || !this.frame.filter.slider)
  19720. throw 'No animation available';
  19721. this.frame.filter.slider.play();
  19722. };
  19723. /**
  19724. * Stop animation
  19725. */
  19726. Graph3d.prototype.animationStop = function() {
  19727. if (!this.frame.filter || !this.frame.filter.slider) return;
  19728. this.frame.filter.slider.stop();
  19729. };
  19730. /**
  19731. * Resize the center position based on the current values in this.defaultXCenter
  19732. * and this.defaultYCenter (which are strings with a percentage or a value
  19733. * in pixels). The center positions are the variables this.xCenter
  19734. * and this.yCenter
  19735. */
  19736. Graph3d.prototype._resizeCenter = function() {
  19737. // calculate the horizontal center position
  19738. if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
  19739. this.xcenter =
  19740. parseFloat(this.defaultXCenter) / 100 *
  19741. this.frame.canvas.clientWidth;
  19742. }
  19743. else {
  19744. this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
  19745. }
  19746. // calculate the vertical center position
  19747. if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
  19748. this.ycenter =
  19749. parseFloat(this.defaultYCenter) / 100 *
  19750. (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
  19751. }
  19752. else {
  19753. this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
  19754. }
  19755. };
  19756. /**
  19757. * Set the rotation and distance of the camera
  19758. * @param {Object} pos An object with the camera position. The object
  19759. * contains three parameters:
  19760. * - horizontal {Number}
  19761. * The horizontal rotation, between 0 and 2*PI.
  19762. * Optional, can be left undefined.
  19763. * - vertical {Number}
  19764. * The vertical rotation, between 0 and 0.5*PI
  19765. * if vertical=0.5*PI, the graph is shown from the
  19766. * top. Optional, can be left undefined.
  19767. * - distance {Number}
  19768. * The (normalized) distance of the camera to the
  19769. * center of the graph, a value between 0.71 and 5.0.
  19770. * Optional, can be left undefined.
  19771. */
  19772. Graph3d.prototype.setCameraPosition = function(pos) {
  19773. if (pos === undefined) {
  19774. return;
  19775. }
  19776. if (pos.horizontal !== undefined && pos.vertical !== undefined) {
  19777. this.camera.setArmRotation(pos.horizontal, pos.vertical);
  19778. }
  19779. if (pos.distance !== undefined) {
  19780. this.camera.setArmLength(pos.distance);
  19781. }
  19782. this.redraw();
  19783. };
  19784. /**
  19785. * Retrieve the current camera rotation
  19786. * @return {object} An object with parameters horizontal, vertical, and
  19787. * distance
  19788. */
  19789. Graph3d.prototype.getCameraPosition = function() {
  19790. var pos = this.camera.getArmRotation();
  19791. pos.distance = this.camera.getArmLength();
  19792. return pos;
  19793. };
  19794. /**
  19795. * Load data into the 3D Graph
  19796. */
  19797. Graph3d.prototype._readData = function(data) {
  19798. // read the data
  19799. this._dataInitialize(data, this.style);
  19800. if (this.dataFilter) {
  19801. // apply filtering
  19802. this.dataPoints = this.dataFilter._getDataPoints();
  19803. }
  19804. else {
  19805. // no filtering. load all data
  19806. this.dataPoints = this._getDataPoints(this.dataTable);
  19807. }
  19808. // draw the filter
  19809. this._redrawFilter();
  19810. };
  19811. /**
  19812. * Replace the dataset of the Graph3d
  19813. * @param {Array | DataSet | DataView} data
  19814. */
  19815. Graph3d.prototype.setData = function (data) {
  19816. this._readData(data);
  19817. this.redraw();
  19818. // start animation when option is true
  19819. if (this.animationAutoStart && this.dataFilter) {
  19820. this.animationStart();
  19821. }
  19822. };
  19823. /**
  19824. * Update the options. Options will be merged with current options
  19825. * @param {Object} options
  19826. */
  19827. Graph3d.prototype.setOptions = function (options) {
  19828. var cameraPosition = undefined;
  19829. this.animationStop();
  19830. if (options !== undefined) {
  19831. // retrieve parameter values
  19832. if (options.width !== undefined) this.width = options.width;
  19833. if (options.height !== undefined) this.height = options.height;
  19834. if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
  19835. if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
  19836. if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
  19837. if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
  19838. if (options.xLabel !== undefined) this.xLabel = options.xLabel;
  19839. if (options.yLabel !== undefined) this.yLabel = options.yLabel;
  19840. if (options.zLabel !== undefined) this.zLabel = options.zLabel;
  19841. if (options.style !== undefined) {
  19842. var styleNumber = this._getStyleNumber(options.style);
  19843. if (styleNumber !== -1) {
  19844. this.style = styleNumber;
  19845. }
  19846. }
  19847. if (options.showGrid !== undefined) this.showGrid = options.showGrid;
  19848. if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
  19849. if (options.showShadow !== undefined) this.showShadow = options.showShadow;
  19850. if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
  19851. if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
  19852. if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
  19853. if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
  19854. if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
  19855. if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
  19856. if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
  19857. if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
  19858. if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
  19859. if (options.xMin !== undefined) this.defaultXMin = options.xMin;
  19860. if (options.xStep !== undefined) this.defaultXStep = options.xStep;
  19861. if (options.xMax !== undefined) this.defaultXMax = options.xMax;
  19862. if (options.yMin !== undefined) this.defaultYMin = options.yMin;
  19863. if (options.yStep !== undefined) this.defaultYStep = options.yStep;
  19864. if (options.yMax !== undefined) this.defaultYMax = options.yMax;
  19865. if (options.zMin !== undefined) this.defaultZMin = options.zMin;
  19866. if (options.zStep !== undefined) this.defaultZStep = options.zStep;
  19867. if (options.zMax !== undefined) this.defaultZMax = options.zMax;
  19868. if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
  19869. if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
  19870. if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
  19871. if (cameraPosition !== undefined) {
  19872. this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
  19873. this.camera.setArmLength(cameraPosition.distance);
  19874. }
  19875. else {
  19876. this.camera.setArmRotation(1.0, 0.5);
  19877. this.camera.setArmLength(1.7);
  19878. }
  19879. }
  19880. this._setBackgroundColor(options && options.backgroundColor);
  19881. this.setSize(this.width, this.height);
  19882. // re-load the data
  19883. if (this.dataTable) {
  19884. this.setData(this.dataTable);
  19885. }
  19886. // start animation when option is true
  19887. if (this.animationAutoStart && this.dataFilter) {
  19888. this.animationStart();
  19889. }
  19890. };
  19891. /**
  19892. * Redraw the Graph.
  19893. */
  19894. Graph3d.prototype.redraw = function() {
  19895. if (this.dataPoints === undefined) {
  19896. throw 'Error: graph data not initialized';
  19897. }
  19898. this._resizeCanvas();
  19899. this._resizeCenter();
  19900. this._redrawSlider();
  19901. this._redrawClear();
  19902. this._redrawAxis();
  19903. if (this.style === Graph3d.STYLE.GRID ||
  19904. this.style === Graph3d.STYLE.SURFACE) {
  19905. this._redrawDataGrid();
  19906. }
  19907. else if (this.style === Graph3d.STYLE.LINE) {
  19908. this._redrawDataLine();
  19909. }
  19910. else if (this.style === Graph3d.STYLE.BAR ||
  19911. this.style === Graph3d.STYLE.BARCOLOR ||
  19912. this.style === Graph3d.STYLE.BARSIZE) {
  19913. this._redrawDataBar();
  19914. }
  19915. else {
  19916. // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
  19917. this._redrawDataDot();
  19918. }
  19919. this._redrawInfo();
  19920. this._redrawLegend();
  19921. };
  19922. /**
  19923. * Clear the canvas before redrawing
  19924. */
  19925. Graph3d.prototype._redrawClear = function() {
  19926. var canvas = this.frame.canvas;
  19927. var ctx = canvas.getContext('2d');
  19928. ctx.clearRect(0, 0, canvas.width, canvas.height);
  19929. };
  19930. /**
  19931. * Redraw the legend showing the colors
  19932. */
  19933. Graph3d.prototype._redrawLegend = function() {
  19934. var y;
  19935. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  19936. this.style === Graph3d.STYLE.DOTSIZE) {
  19937. var dotSize = this.frame.clientWidth * 0.02;
  19938. var widthMin, widthMax;
  19939. if (this.style === Graph3d.STYLE.DOTSIZE) {
  19940. widthMin = dotSize / 2; // px
  19941. widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
  19942. }
  19943. else {
  19944. widthMin = 20; // px
  19945. widthMax = 20; // px
  19946. }
  19947. var height = Math.max(this.frame.clientHeight * 0.25, 100);
  19948. var top = this.margin;
  19949. var right = this.frame.clientWidth - this.margin;
  19950. var left = right - widthMax;
  19951. var bottom = top + height;
  19952. }
  19953. var canvas = this.frame.canvas;
  19954. var ctx = canvas.getContext('2d');
  19955. ctx.lineWidth = 1;
  19956. ctx.font = '14px arial'; // TODO: put in options
  19957. if (this.style === Graph3d.STYLE.DOTCOLOR) {
  19958. // draw the color bar
  19959. var ymin = 0;
  19960. var ymax = height; // Todo: make height customizable
  19961. for (y = ymin; y < ymax; y++) {
  19962. var f = (y - ymin) / (ymax - ymin);
  19963. //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
  19964. var hue = f * 240;
  19965. var color = this._hsv2rgb(hue, 1, 1);
  19966. ctx.strokeStyle = color;
  19967. ctx.beginPath();
  19968. ctx.moveTo(left, top + y);
  19969. ctx.lineTo(right, top + y);
  19970. ctx.stroke();
  19971. }
  19972. ctx.strokeStyle = this.colorAxis;
  19973. ctx.strokeRect(left, top, widthMax, height);
  19974. }
  19975. if (this.style === Graph3d.STYLE.DOTSIZE) {
  19976. // draw border around color bar
  19977. ctx.strokeStyle = this.colorAxis;
  19978. ctx.fillStyle = this.colorDot;
  19979. ctx.beginPath();
  19980. ctx.moveTo(left, top);
  19981. ctx.lineTo(right, top);
  19982. ctx.lineTo(right - widthMax + widthMin, bottom);
  19983. ctx.lineTo(left, bottom);
  19984. ctx.closePath();
  19985. ctx.fill();
  19986. ctx.stroke();
  19987. }
  19988. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  19989. this.style === Graph3d.STYLE.DOTSIZE) {
  19990. // print values along the color bar
  19991. var gridLineLen = 5; // px
  19992. var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
  19993. step.start();
  19994. if (step.getCurrent() < this.valueMin) {
  19995. step.next();
  19996. }
  19997. while (!step.end()) {
  19998. y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
  19999. ctx.beginPath();
  20000. ctx.moveTo(left - gridLineLen, y);
  20001. ctx.lineTo(left, y);
  20002. ctx.stroke();
  20003. ctx.textAlign = 'right';
  20004. ctx.textBaseline = 'middle';
  20005. ctx.fillStyle = this.colorAxis;
  20006. ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
  20007. step.next();
  20008. }
  20009. ctx.textAlign = 'right';
  20010. ctx.textBaseline = 'top';
  20011. var label = this.legendLabel;
  20012. ctx.fillText(label, right, bottom + this.margin);
  20013. }
  20014. };
  20015. /**
  20016. * Redraw the filter
  20017. */
  20018. Graph3d.prototype._redrawFilter = function() {
  20019. this.frame.filter.innerHTML = '';
  20020. if (this.dataFilter) {
  20021. var options = {
  20022. 'visible': this.showAnimationControls
  20023. };
  20024. var slider = new Slider(this.frame.filter, options);
  20025. this.frame.filter.slider = slider;
  20026. // TODO: css here is not nice here...
  20027. this.frame.filter.style.padding = '10px';
  20028. //this.frame.filter.style.backgroundColor = '#EFEFEF';
  20029. slider.setValues(this.dataFilter.values);
  20030. slider.setPlayInterval(this.animationInterval);
  20031. // create an event handler
  20032. var me = this;
  20033. var onchange = function () {
  20034. var index = slider.getIndex();
  20035. me.dataFilter.selectValue(index);
  20036. me.dataPoints = me.dataFilter._getDataPoints();
  20037. me.redraw();
  20038. };
  20039. slider.setOnChangeCallback(onchange);
  20040. }
  20041. else {
  20042. this.frame.filter.slider = undefined;
  20043. }
  20044. };
  20045. /**
  20046. * Redraw the slider
  20047. */
  20048. Graph3d.prototype._redrawSlider = function() {
  20049. if ( this.frame.filter.slider !== undefined) {
  20050. this.frame.filter.slider.redraw();
  20051. }
  20052. };
  20053. /**
  20054. * Redraw common information
  20055. */
  20056. Graph3d.prototype._redrawInfo = function() {
  20057. if (this.dataFilter) {
  20058. var canvas = this.frame.canvas;
  20059. var ctx = canvas.getContext('2d');
  20060. ctx.font = '14px arial'; // TODO: put in options
  20061. ctx.lineStyle = 'gray';
  20062. ctx.fillStyle = 'gray';
  20063. ctx.textAlign = 'left';
  20064. ctx.textBaseline = 'top';
  20065. var x = this.margin;
  20066. var y = this.margin;
  20067. ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
  20068. }
  20069. };
  20070. /**
  20071. * Redraw the axis
  20072. */
  20073. Graph3d.prototype._redrawAxis = function() {
  20074. var canvas = this.frame.canvas,
  20075. ctx = canvas.getContext('2d'),
  20076. from, to, step, prettyStep,
  20077. text, xText, yText, zText,
  20078. offset, xOffset, yOffset,
  20079. xMin2d, xMax2d;
  20080. // TODO: get the actual rendered style of the containerElement
  20081. //ctx.font = this.containerElement.style.font;
  20082. ctx.font = 24 / this.camera.getArmLength() + 'px arial';
  20083. // calculate the length for the short grid lines
  20084. var gridLenX = 0.025 / this.scale.x;
  20085. var gridLenY = 0.025 / this.scale.y;
  20086. var textMargin = 5 / this.camera.getArmLength(); // px
  20087. var armAngle = this.camera.getArmRotation().horizontal;
  20088. // draw x-grid lines
  20089. ctx.lineWidth = 1;
  20090. prettyStep = (this.defaultXStep === undefined);
  20091. step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
  20092. step.start();
  20093. if (step.getCurrent() < this.xMin) {
  20094. step.next();
  20095. }
  20096. while (!step.end()) {
  20097. var x = step.getCurrent();
  20098. if (this.showGrid) {
  20099. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  20100. to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  20101. ctx.strokeStyle = this.colorGrid;
  20102. ctx.beginPath();
  20103. ctx.moveTo(from.x, from.y);
  20104. ctx.lineTo(to.x, to.y);
  20105. ctx.stroke();
  20106. }
  20107. else {
  20108. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  20109. to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
  20110. ctx.strokeStyle = this.colorAxis;
  20111. ctx.beginPath();
  20112. ctx.moveTo(from.x, from.y);
  20113. ctx.lineTo(to.x, to.y);
  20114. ctx.stroke();
  20115. from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  20116. to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
  20117. ctx.strokeStyle = this.colorAxis;
  20118. ctx.beginPath();
  20119. ctx.moveTo(from.x, from.y);
  20120. ctx.lineTo(to.x, to.y);
  20121. ctx.stroke();
  20122. }
  20123. yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
  20124. text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
  20125. if (Math.cos(armAngle * 2) > 0) {
  20126. ctx.textAlign = 'center';
  20127. ctx.textBaseline = 'top';
  20128. text.y += textMargin;
  20129. }
  20130. else if (Math.sin(armAngle * 2) < 0){
  20131. ctx.textAlign = 'right';
  20132. ctx.textBaseline = 'middle';
  20133. }
  20134. else {
  20135. ctx.textAlign = 'left';
  20136. ctx.textBaseline = 'middle';
  20137. }
  20138. ctx.fillStyle = this.colorAxis;
  20139. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  20140. step.next();
  20141. }
  20142. // draw y-grid lines
  20143. ctx.lineWidth = 1;
  20144. prettyStep = (this.defaultYStep === undefined);
  20145. step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
  20146. step.start();
  20147. if (step.getCurrent() < this.yMin) {
  20148. step.next();
  20149. }
  20150. while (!step.end()) {
  20151. if (this.showGrid) {
  20152. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  20153. to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  20154. ctx.strokeStyle = this.colorGrid;
  20155. ctx.beginPath();
  20156. ctx.moveTo(from.x, from.y);
  20157. ctx.lineTo(to.x, to.y);
  20158. ctx.stroke();
  20159. }
  20160. else {
  20161. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  20162. to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
  20163. ctx.strokeStyle = this.colorAxis;
  20164. ctx.beginPath();
  20165. ctx.moveTo(from.x, from.y);
  20166. ctx.lineTo(to.x, to.y);
  20167. ctx.stroke();
  20168. from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  20169. to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
  20170. ctx.strokeStyle = this.colorAxis;
  20171. ctx.beginPath();
  20172. ctx.moveTo(from.x, from.y);
  20173. ctx.lineTo(to.x, to.y);
  20174. ctx.stroke();
  20175. }
  20176. xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
  20177. text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
  20178. if (Math.cos(armAngle * 2) < 0) {
  20179. ctx.textAlign = 'center';
  20180. ctx.textBaseline = 'top';
  20181. text.y += textMargin;
  20182. }
  20183. else if (Math.sin(armAngle * 2) > 0){
  20184. ctx.textAlign = 'right';
  20185. ctx.textBaseline = 'middle';
  20186. }
  20187. else {
  20188. ctx.textAlign = 'left';
  20189. ctx.textBaseline = 'middle';
  20190. }
  20191. ctx.fillStyle = this.colorAxis;
  20192. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  20193. step.next();
  20194. }
  20195. // draw z-grid lines and axis
  20196. ctx.lineWidth = 1;
  20197. prettyStep = (this.defaultZStep === undefined);
  20198. step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
  20199. step.start();
  20200. if (step.getCurrent() < this.zMin) {
  20201. step.next();
  20202. }
  20203. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  20204. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  20205. while (!step.end()) {
  20206. // TODO: make z-grid lines really 3d?
  20207. from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
  20208. ctx.strokeStyle = this.colorAxis;
  20209. ctx.beginPath();
  20210. ctx.moveTo(from.x, from.y);
  20211. ctx.lineTo(from.x - textMargin, from.y);
  20212. ctx.stroke();
  20213. ctx.textAlign = 'right';
  20214. ctx.textBaseline = 'middle';
  20215. ctx.fillStyle = this.colorAxis;
  20216. ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
  20217. step.next();
  20218. }
  20219. ctx.lineWidth = 1;
  20220. from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  20221. to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
  20222. ctx.strokeStyle = this.colorAxis;
  20223. ctx.beginPath();
  20224. ctx.moveTo(from.x, from.y);
  20225. ctx.lineTo(to.x, to.y);
  20226. ctx.stroke();
  20227. // draw x-axis
  20228. ctx.lineWidth = 1;
  20229. // line at yMin
  20230. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  20231. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  20232. ctx.strokeStyle = this.colorAxis;
  20233. ctx.beginPath();
  20234. ctx.moveTo(xMin2d.x, xMin2d.y);
  20235. ctx.lineTo(xMax2d.x, xMax2d.y);
  20236. ctx.stroke();
  20237. // line at ymax
  20238. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  20239. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  20240. ctx.strokeStyle = this.colorAxis;
  20241. ctx.beginPath();
  20242. ctx.moveTo(xMin2d.x, xMin2d.y);
  20243. ctx.lineTo(xMax2d.x, xMax2d.y);
  20244. ctx.stroke();
  20245. // draw y-axis
  20246. ctx.lineWidth = 1;
  20247. // line at xMin
  20248. from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  20249. to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  20250. ctx.strokeStyle = this.colorAxis;
  20251. ctx.beginPath();
  20252. ctx.moveTo(from.x, from.y);
  20253. ctx.lineTo(to.x, to.y);
  20254. ctx.stroke();
  20255. // line at xMax
  20256. from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  20257. to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  20258. ctx.strokeStyle = this.colorAxis;
  20259. ctx.beginPath();
  20260. ctx.moveTo(from.x, from.y);
  20261. ctx.lineTo(to.x, to.y);
  20262. ctx.stroke();
  20263. // draw x-label
  20264. var xLabel = this.xLabel;
  20265. if (xLabel.length > 0) {
  20266. yOffset = 0.1 / this.scale.y;
  20267. xText = (this.xMin + this.xMax) / 2;
  20268. yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
  20269. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  20270. if (Math.cos(armAngle * 2) > 0) {
  20271. ctx.textAlign = 'center';
  20272. ctx.textBaseline = 'top';
  20273. }
  20274. else if (Math.sin(armAngle * 2) < 0){
  20275. ctx.textAlign = 'right';
  20276. ctx.textBaseline = 'middle';
  20277. }
  20278. else {
  20279. ctx.textAlign = 'left';
  20280. ctx.textBaseline = 'middle';
  20281. }
  20282. ctx.fillStyle = this.colorAxis;
  20283. ctx.fillText(xLabel, text.x, text.y);
  20284. }
  20285. // draw y-label
  20286. var yLabel = this.yLabel;
  20287. if (yLabel.length > 0) {
  20288. xOffset = 0.1 / this.scale.x;
  20289. xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
  20290. yText = (this.yMin + this.yMax) / 2;
  20291. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  20292. if (Math.cos(armAngle * 2) < 0) {
  20293. ctx.textAlign = 'center';
  20294. ctx.textBaseline = 'top';
  20295. }
  20296. else if (Math.sin(armAngle * 2) > 0){
  20297. ctx.textAlign = 'right';
  20298. ctx.textBaseline = 'middle';
  20299. }
  20300. else {
  20301. ctx.textAlign = 'left';
  20302. ctx.textBaseline = 'middle';
  20303. }
  20304. ctx.fillStyle = this.colorAxis;
  20305. ctx.fillText(yLabel, text.x, text.y);
  20306. }
  20307. // draw z-label
  20308. var zLabel = this.zLabel;
  20309. if (zLabel.length > 0) {
  20310. offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
  20311. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  20312. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  20313. zText = (this.zMin + this.zMax) / 2;
  20314. text = this._convert3Dto2D(new Point3d(xText, yText, zText));
  20315. ctx.textAlign = 'right';
  20316. ctx.textBaseline = 'middle';
  20317. ctx.fillStyle = this.colorAxis;
  20318. ctx.fillText(zLabel, text.x - offset, text.y);
  20319. }
  20320. };
  20321. /**
  20322. * Calculate the color based on the given value.
  20323. * @param {Number} H Hue, a value be between 0 and 360
  20324. * @param {Number} S Saturation, a value between 0 and 1
  20325. * @param {Number} V Value, a value between 0 and 1
  20326. */
  20327. Graph3d.prototype._hsv2rgb = function(H, S, V) {
  20328. var R, G, B, C, Hi, X;
  20329. C = V * S;
  20330. Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
  20331. X = C * (1 - Math.abs(((H/60) % 2) - 1));
  20332. switch (Hi) {
  20333. case 0: R = C; G = X; B = 0; break;
  20334. case 1: R = X; G = C; B = 0; break;
  20335. case 2: R = 0; G = C; B = X; break;
  20336. case 3: R = 0; G = X; B = C; break;
  20337. case 4: R = X; G = 0; B = C; break;
  20338. case 5: R = C; G = 0; B = X; break;
  20339. default: R = 0; G = 0; B = 0; break;
  20340. }
  20341. return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
  20342. };
  20343. /**
  20344. * Draw all datapoints as a grid
  20345. * This function can be used when the style is 'grid'
  20346. */
  20347. Graph3d.prototype._redrawDataGrid = function() {
  20348. var canvas = this.frame.canvas,
  20349. ctx = canvas.getContext('2d'),
  20350. point, right, top, cross,
  20351. i,
  20352. topSideVisible, fillStyle, strokeStyle, lineWidth,
  20353. h, s, v, zAvg;
  20354. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20355. return; // TODO: throw exception?
  20356. // calculate the translations and screen position of all points
  20357. for (i = 0; i < this.dataPoints.length; i++) {
  20358. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20359. var screen = this._convertTranslationToScreen(trans);
  20360. this.dataPoints[i].trans = trans;
  20361. this.dataPoints[i].screen = screen;
  20362. // calculate the translation of the point at the bottom (needed for sorting)
  20363. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  20364. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  20365. }
  20366. // sort the points on depth of their (x,y) position (not on z)
  20367. var sortDepth = function (a, b) {
  20368. return b.dist - a.dist;
  20369. };
  20370. this.dataPoints.sort(sortDepth);
  20371. if (this.style === Graph3d.STYLE.SURFACE) {
  20372. for (i = 0; i < this.dataPoints.length; i++) {
  20373. point = this.dataPoints[i];
  20374. right = this.dataPoints[i].pointRight;
  20375. top = this.dataPoints[i].pointTop;
  20376. cross = this.dataPoints[i].pointCross;
  20377. if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
  20378. if (this.showGrayBottom || this.showShadow) {
  20379. // calculate the cross product of the two vectors from center
  20380. // to left and right, in order to know whether we are looking at the
  20381. // bottom or at the top side. We can also use the cross product
  20382. // for calculating light intensity
  20383. var aDiff = Point3d.subtract(cross.trans, point.trans);
  20384. var bDiff = Point3d.subtract(top.trans, right.trans);
  20385. var crossproduct = Point3d.crossProduct(aDiff, bDiff);
  20386. var len = crossproduct.length();
  20387. // FIXME: there is a bug with determining the surface side (shadow or colored)
  20388. topSideVisible = (crossproduct.z > 0);
  20389. }
  20390. else {
  20391. topSideVisible = true;
  20392. }
  20393. if (topSideVisible) {
  20394. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20395. zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
  20396. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20397. s = 1; // saturation
  20398. if (this.showShadow) {
  20399. v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
  20400. fillStyle = this._hsv2rgb(h, s, v);
  20401. strokeStyle = fillStyle;
  20402. }
  20403. else {
  20404. v = 1;
  20405. fillStyle = this._hsv2rgb(h, s, v);
  20406. strokeStyle = this.colorAxis;
  20407. }
  20408. }
  20409. else {
  20410. fillStyle = 'gray';
  20411. strokeStyle = this.colorAxis;
  20412. }
  20413. lineWidth = 0.5;
  20414. ctx.lineWidth = lineWidth;
  20415. ctx.fillStyle = fillStyle;
  20416. ctx.strokeStyle = strokeStyle;
  20417. ctx.beginPath();
  20418. ctx.moveTo(point.screen.x, point.screen.y);
  20419. ctx.lineTo(right.screen.x, right.screen.y);
  20420. ctx.lineTo(cross.screen.x, cross.screen.y);
  20421. ctx.lineTo(top.screen.x, top.screen.y);
  20422. ctx.closePath();
  20423. ctx.fill();
  20424. ctx.stroke();
  20425. }
  20426. }
  20427. }
  20428. else { // grid style
  20429. for (i = 0; i < this.dataPoints.length; i++) {
  20430. point = this.dataPoints[i];
  20431. right = this.dataPoints[i].pointRight;
  20432. top = this.dataPoints[i].pointTop;
  20433. if (point !== undefined) {
  20434. if (this.showPerspective) {
  20435. lineWidth = 2 / -point.trans.z;
  20436. }
  20437. else {
  20438. lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
  20439. }
  20440. }
  20441. if (point !== undefined && right !== undefined) {
  20442. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20443. zAvg = (point.point.z + right.point.z) / 2;
  20444. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20445. ctx.lineWidth = lineWidth;
  20446. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  20447. ctx.beginPath();
  20448. ctx.moveTo(point.screen.x, point.screen.y);
  20449. ctx.lineTo(right.screen.x, right.screen.y);
  20450. ctx.stroke();
  20451. }
  20452. if (point !== undefined && top !== undefined) {
  20453. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20454. zAvg = (point.point.z + top.point.z) / 2;
  20455. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20456. ctx.lineWidth = lineWidth;
  20457. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  20458. ctx.beginPath();
  20459. ctx.moveTo(point.screen.x, point.screen.y);
  20460. ctx.lineTo(top.screen.x, top.screen.y);
  20461. ctx.stroke();
  20462. }
  20463. }
  20464. }
  20465. };
  20466. /**
  20467. * Draw all datapoints as dots.
  20468. * This function can be used when the style is 'dot' or 'dot-line'
  20469. */
  20470. Graph3d.prototype._redrawDataDot = function() {
  20471. var canvas = this.frame.canvas;
  20472. var ctx = canvas.getContext('2d');
  20473. var i;
  20474. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20475. return; // TODO: throw exception?
  20476. // calculate the translations of all points
  20477. for (i = 0; i < this.dataPoints.length; i++) {
  20478. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20479. var screen = this._convertTranslationToScreen(trans);
  20480. this.dataPoints[i].trans = trans;
  20481. this.dataPoints[i].screen = screen;
  20482. // calculate the distance from the point at the bottom to the camera
  20483. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  20484. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  20485. }
  20486. // order the translated points by depth
  20487. var sortDepth = function (a, b) {
  20488. return b.dist - a.dist;
  20489. };
  20490. this.dataPoints.sort(sortDepth);
  20491. // draw the datapoints as colored circles
  20492. var dotSize = this.frame.clientWidth * 0.02; // px
  20493. for (i = 0; i < this.dataPoints.length; i++) {
  20494. var point = this.dataPoints[i];
  20495. if (this.style === Graph3d.STYLE.DOTLINE) {
  20496. // draw a vertical line from the bottom to the graph value
  20497. //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
  20498. var from = this._convert3Dto2D(point.bottom);
  20499. ctx.lineWidth = 1;
  20500. ctx.strokeStyle = this.colorGrid;
  20501. ctx.beginPath();
  20502. ctx.moveTo(from.x, from.y);
  20503. ctx.lineTo(point.screen.x, point.screen.y);
  20504. ctx.stroke();
  20505. }
  20506. // calculate radius for the circle
  20507. var size;
  20508. if (this.style === Graph3d.STYLE.DOTSIZE) {
  20509. size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
  20510. }
  20511. else {
  20512. size = dotSize;
  20513. }
  20514. var radius;
  20515. if (this.showPerspective) {
  20516. radius = size / -point.trans.z;
  20517. }
  20518. else {
  20519. radius = size * -(this.eye.z / this.camera.getArmLength());
  20520. }
  20521. if (radius < 0) {
  20522. radius = 0;
  20523. }
  20524. var hue, color, borderColor;
  20525. if (this.style === Graph3d.STYLE.DOTCOLOR ) {
  20526. // calculate the color based on the value
  20527. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  20528. color = this._hsv2rgb(hue, 1, 1);
  20529. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20530. }
  20531. else if (this.style === Graph3d.STYLE.DOTSIZE) {
  20532. color = this.colorDot;
  20533. borderColor = this.colorDotBorder;
  20534. }
  20535. else {
  20536. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20537. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20538. color = this._hsv2rgb(hue, 1, 1);
  20539. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20540. }
  20541. // draw the circle
  20542. ctx.lineWidth = 1.0;
  20543. ctx.strokeStyle = borderColor;
  20544. ctx.fillStyle = color;
  20545. ctx.beginPath();
  20546. ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
  20547. ctx.fill();
  20548. ctx.stroke();
  20549. }
  20550. };
  20551. /**
  20552. * Draw all datapoints as bars.
  20553. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
  20554. */
  20555. Graph3d.prototype._redrawDataBar = function() {
  20556. var canvas = this.frame.canvas;
  20557. var ctx = canvas.getContext('2d');
  20558. var i, j, surface, corners;
  20559. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20560. return; // TODO: throw exception?
  20561. // calculate the translations of all points
  20562. for (i = 0; i < this.dataPoints.length; i++) {
  20563. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20564. var screen = this._convertTranslationToScreen(trans);
  20565. this.dataPoints[i].trans = trans;
  20566. this.dataPoints[i].screen = screen;
  20567. // calculate the distance from the point at the bottom to the camera
  20568. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  20569. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  20570. }
  20571. // order the translated points by depth
  20572. var sortDepth = function (a, b) {
  20573. return b.dist - a.dist;
  20574. };
  20575. this.dataPoints.sort(sortDepth);
  20576. // draw the datapoints as bars
  20577. var xWidth = this.xBarWidth / 2;
  20578. var yWidth = this.yBarWidth / 2;
  20579. for (i = 0; i < this.dataPoints.length; i++) {
  20580. var point = this.dataPoints[i];
  20581. // determine color
  20582. var hue, color, borderColor;
  20583. if (this.style === Graph3d.STYLE.BARCOLOR ) {
  20584. // calculate the color based on the value
  20585. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  20586. color = this._hsv2rgb(hue, 1, 1);
  20587. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20588. }
  20589. else if (this.style === Graph3d.STYLE.BARSIZE) {
  20590. color = this.colorDot;
  20591. borderColor = this.colorDotBorder;
  20592. }
  20593. else {
  20594. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20595. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20596. color = this._hsv2rgb(hue, 1, 1);
  20597. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20598. }
  20599. // calculate size for the bar
  20600. if (this.style === Graph3d.STYLE.BARSIZE) {
  20601. xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  20602. yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  20603. }
  20604. // calculate all corner points
  20605. var me = this;
  20606. var point3d = point.point;
  20607. var top = [
  20608. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
  20609. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
  20610. {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
  20611. {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
  20612. ];
  20613. var bottom = [
  20614. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
  20615. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
  20616. {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
  20617. {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
  20618. ];
  20619. // calculate screen location of the points
  20620. top.forEach(function (obj) {
  20621. obj.screen = me._convert3Dto2D(obj.point);
  20622. });
  20623. bottom.forEach(function (obj) {
  20624. obj.screen = me._convert3Dto2D(obj.point);
  20625. });
  20626. // create five sides, calculate both corner points and center points
  20627. var surfaces = [
  20628. {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
  20629. {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
  20630. {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
  20631. {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
  20632. {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
  20633. ];
  20634. point.surfaces = surfaces;
  20635. // calculate the distance of each of the surface centers to the camera
  20636. for (j = 0; j < surfaces.length; j++) {
  20637. surface = surfaces[j];
  20638. var transCenter = this._convertPointToTranslation(surface.center);
  20639. surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
  20640. // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
  20641. // but the current solution is fast/simple and works in 99.9% of all cases
  20642. // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
  20643. }
  20644. // order the surfaces by their (translated) depth
  20645. surfaces.sort(function (a, b) {
  20646. var diff = b.dist - a.dist;
  20647. if (diff) return diff;
  20648. // if equal depth, sort the top surface last
  20649. if (a.corners === top) return 1;
  20650. if (b.corners === top) return -1;
  20651. // both are equal
  20652. return 0;
  20653. });
  20654. // draw the ordered surfaces
  20655. ctx.lineWidth = 1;
  20656. ctx.strokeStyle = borderColor;
  20657. ctx.fillStyle = color;
  20658. // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
  20659. for (j = 2; j < surfaces.length; j++) {
  20660. surface = surfaces[j];
  20661. corners = surface.corners;
  20662. ctx.beginPath();
  20663. ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
  20664. ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
  20665. ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
  20666. ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
  20667. ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
  20668. ctx.fill();
  20669. ctx.stroke();
  20670. }
  20671. }
  20672. };
  20673. /**
  20674. * Draw a line through all datapoints.
  20675. * This function can be used when the style is 'line'
  20676. */
  20677. Graph3d.prototype._redrawDataLine = function() {
  20678. var canvas = this.frame.canvas,
  20679. ctx = canvas.getContext('2d'),
  20680. point, i;
  20681. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20682. return; // TODO: throw exception?
  20683. // calculate the translations of all points
  20684. for (i = 0; i < this.dataPoints.length; i++) {
  20685. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20686. var screen = this._convertTranslationToScreen(trans);
  20687. this.dataPoints[i].trans = trans;
  20688. this.dataPoints[i].screen = screen;
  20689. }
  20690. // start the line
  20691. if (this.dataPoints.length > 0) {
  20692. point = this.dataPoints[0];
  20693. ctx.lineWidth = 1; // TODO: make customizable
  20694. ctx.strokeStyle = 'blue'; // TODO: make customizable
  20695. ctx.beginPath();
  20696. ctx.moveTo(point.screen.x, point.screen.y);
  20697. }
  20698. // draw the datapoints as colored circles
  20699. for (i = 1; i < this.dataPoints.length; i++) {
  20700. point = this.dataPoints[i];
  20701. ctx.lineTo(point.screen.x, point.screen.y);
  20702. }
  20703. // finish the line
  20704. if (this.dataPoints.length > 0) {
  20705. ctx.stroke();
  20706. }
  20707. };
  20708. /**
  20709. * Start a moving operation inside the provided parent element
  20710. * @param {Event} event The event that occurred (required for
  20711. * retrieving the mouse position)
  20712. */
  20713. Graph3d.prototype._onMouseDown = function(event) {
  20714. event = event || window.event;
  20715. // check if mouse is still down (may be up when focus is lost for example
  20716. // in an iframe)
  20717. if (this.leftButtonDown) {
  20718. this._onMouseUp(event);
  20719. }
  20720. // only react on left mouse button down
  20721. this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  20722. if (!this.leftButtonDown && !this.touchDown) return;
  20723. // get mouse position (different code for IE and all other browsers)
  20724. this.startMouseX = getMouseX(event);
  20725. this.startMouseY = getMouseY(event);
  20726. this.startStart = new Date(this.start);
  20727. this.startEnd = new Date(this.end);
  20728. this.startArmRotation = this.camera.getArmRotation();
  20729. this.frame.style.cursor = 'move';
  20730. // add event listeners to handle moving the contents
  20731. // we store the function onmousemove and onmouseup in the graph, so we can
  20732. // remove the eventlisteners lateron in the function mouseUp()
  20733. var me = this;
  20734. this.onmousemove = function (event) {me._onMouseMove(event);};
  20735. this.onmouseup = function (event) {me._onMouseUp(event);};
  20736. G3DaddEventListener(document, 'mousemove', me.onmousemove);
  20737. G3DaddEventListener(document, 'mouseup', me.onmouseup);
  20738. G3DpreventDefault(event);
  20739. };
  20740. /**
  20741. * Perform moving operating.
  20742. * This function activated from within the funcion Graph.mouseDown().
  20743. * @param {Event} event Well, eehh, the event
  20744. */
  20745. Graph3d.prototype._onMouseMove = function (event) {
  20746. event = event || window.event;
  20747. // calculate change in mouse position
  20748. var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
  20749. var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
  20750. var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
  20751. var verticalNew = this.startArmRotation.vertical + diffY / 200;
  20752. var snapAngle = 4; // degrees
  20753. var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
  20754. // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
  20755. // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
  20756. if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
  20757. horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
  20758. }
  20759. if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
  20760. horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
  20761. }
  20762. // snap vertically to nice angles
  20763. if (Math.abs(Math.sin(verticalNew)) < snapValue) {
  20764. verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
  20765. }
  20766. if (Math.abs(Math.cos(verticalNew)) < snapValue) {
  20767. verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
  20768. }
  20769. this.camera.setArmRotation(horizontalNew, verticalNew);
  20770. this.redraw();
  20771. // fire a cameraPositionChange event
  20772. var parameters = this.getCameraPosition();
  20773. this.emit('cameraPositionChange', parameters);
  20774. G3DpreventDefault(event);
  20775. };
  20776. /**
  20777. * Stop moving operating.
  20778. * This function activated from within the funcion Graph.mouseDown().
  20779. * @param {event} event The event
  20780. */
  20781. Graph3d.prototype._onMouseUp = function (event) {
  20782. this.frame.style.cursor = 'auto';
  20783. this.leftButtonDown = false;
  20784. // remove event listeners here
  20785. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  20786. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  20787. G3DpreventDefault(event);
  20788. };
  20789. /**
  20790. * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
  20791. * @param {Event} event A mouse move event
  20792. */
  20793. Graph3d.prototype._onTooltip = function (event) {
  20794. var delay = 300; // ms
  20795. var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
  20796. var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
  20797. if (!this.showTooltip) {
  20798. return;
  20799. }
  20800. if (this.tooltipTimeout) {
  20801. clearTimeout(this.tooltipTimeout);
  20802. }
  20803. // (delayed) display of a tooltip only if no mouse button is down
  20804. if (this.leftButtonDown) {
  20805. this._hideTooltip();
  20806. return;
  20807. }
  20808. if (this.tooltip && this.tooltip.dataPoint) {
  20809. // tooltip is currently visible
  20810. var dataPoint = this._dataPointFromXY(mouseX, mouseY);
  20811. if (dataPoint !== this.tooltip.dataPoint) {
  20812. // datapoint changed
  20813. if (dataPoint) {
  20814. this._showTooltip(dataPoint);
  20815. }
  20816. else {
  20817. this._hideTooltip();
  20818. }
  20819. }
  20820. }
  20821. else {
  20822. // tooltip is currently not visible
  20823. var me = this;
  20824. this.tooltipTimeout = setTimeout(function () {
  20825. me.tooltipTimeout = null;
  20826. // show a tooltip if we have a data point
  20827. var dataPoint = me._dataPointFromXY(mouseX, mouseY);
  20828. if (dataPoint) {
  20829. me._showTooltip(dataPoint);
  20830. }
  20831. }, delay);
  20832. }
  20833. };
  20834. /**
  20835. * Event handler for touchstart event on mobile devices
  20836. */
  20837. Graph3d.prototype._onTouchStart = function(event) {
  20838. this.touchDown = true;
  20839. var me = this;
  20840. this.ontouchmove = function (event) {me._onTouchMove(event);};
  20841. this.ontouchend = function (event) {me._onTouchEnd(event);};
  20842. G3DaddEventListener(document, 'touchmove', me.ontouchmove);
  20843. G3DaddEventListener(document, 'touchend', me.ontouchend);
  20844. this._onMouseDown(event);
  20845. };
  20846. /**
  20847. * Event handler for touchmove event on mobile devices
  20848. */
  20849. Graph3d.prototype._onTouchMove = function(event) {
  20850. this._onMouseMove(event);
  20851. };
  20852. /**
  20853. * Event handler for touchend event on mobile devices
  20854. */
  20855. Graph3d.prototype._onTouchEnd = function(event) {
  20856. this.touchDown = false;
  20857. G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
  20858. G3DremoveEventListener(document, 'touchend', this.ontouchend);
  20859. this._onMouseUp(event);
  20860. };
  20861. /**
  20862. * Event handler for mouse wheel event, used to zoom the graph
  20863. * Code from http://adomas.org/javascript-mouse-wheel/
  20864. * @param {event} event The event
  20865. */
  20866. Graph3d.prototype._onWheel = function(event) {
  20867. if (!event) /* For IE. */
  20868. event = window.event;
  20869. // retrieve delta
  20870. var delta = 0;
  20871. if (event.wheelDelta) { /* IE/Opera. */
  20872. delta = event.wheelDelta/120;
  20873. } else if (event.detail) { /* Mozilla case. */
  20874. // In Mozilla, sign of delta is different than in IE.
  20875. // Also, delta is multiple of 3.
  20876. delta = -event.detail/3;
  20877. }
  20878. // If delta is nonzero, handle it.
  20879. // Basically, delta is now positive if wheel was scrolled up,
  20880. // and negative, if wheel was scrolled down.
  20881. if (delta) {
  20882. var oldLength = this.camera.getArmLength();
  20883. var newLength = oldLength * (1 - delta / 10);
  20884. this.camera.setArmLength(newLength);
  20885. this.redraw();
  20886. this._hideTooltip();
  20887. }
  20888. // fire a cameraPositionChange event
  20889. var parameters = this.getCameraPosition();
  20890. this.emit('cameraPositionChange', parameters);
  20891. // Prevent default actions caused by mouse wheel.
  20892. // That might be ugly, but we handle scrolls somehow
  20893. // anyway, so don't bother here..
  20894. G3DpreventDefault(event);
  20895. };
  20896. /**
  20897. * Test whether a point lies inside given 2D triangle
  20898. * @param {Point2d} point
  20899. * @param {Point2d[]} triangle
  20900. * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
  20901. * @private
  20902. */
  20903. Graph3d.prototype._insideTriangle = function (point, triangle) {
  20904. var a = triangle[0],
  20905. b = triangle[1],
  20906. c = triangle[2];
  20907. function sign (x) {
  20908. return x > 0 ? 1 : x < 0 ? -1 : 0;
  20909. }
  20910. var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
  20911. var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
  20912. var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
  20913. // each of the three signs must be either equal to each other or zero
  20914. return (as == 0 || bs == 0 || as == bs) &&
  20915. (bs == 0 || cs == 0 || bs == cs) &&
  20916. (as == 0 || cs == 0 || as == cs);
  20917. };
  20918. /**
  20919. * Find a data point close to given screen position (x, y)
  20920. * @param {Number} x
  20921. * @param {Number} y
  20922. * @return {Object | null} The closest data point or null if not close to any data point
  20923. * @private
  20924. */
  20925. Graph3d.prototype._dataPointFromXY = function (x, y) {
  20926. var i,
  20927. distMax = 100, // px
  20928. dataPoint = null,
  20929. closestDataPoint = null,
  20930. closestDist = null,
  20931. center = new Point2d(x, y);
  20932. if (this.style === Graph3d.STYLE.BAR ||
  20933. this.style === Graph3d.STYLE.BARCOLOR ||
  20934. this.style === Graph3d.STYLE.BARSIZE) {
  20935. // the data points are ordered from far away to closest
  20936. for (i = this.dataPoints.length - 1; i >= 0; i--) {
  20937. dataPoint = this.dataPoints[i];
  20938. var surfaces = dataPoint.surfaces;
  20939. if (surfaces) {
  20940. for (var s = surfaces.length - 1; s >= 0; s--) {
  20941. // split each surface in two triangles, and see if the center point is inside one of these
  20942. var surface = surfaces[s];
  20943. var corners = surface.corners;
  20944. var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
  20945. var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
  20946. if (this._insideTriangle(center, triangle1) ||
  20947. this._insideTriangle(center, triangle2)) {
  20948. // return immediately at the first hit
  20949. return dataPoint;
  20950. }
  20951. }
  20952. }
  20953. }
  20954. }
  20955. else {
  20956. // find the closest data point, using distance to the center of the point on 2d screen
  20957. for (i = 0; i < this.dataPoints.length; i++) {
  20958. dataPoint = this.dataPoints[i];
  20959. var point = dataPoint.screen;
  20960. if (point) {
  20961. var distX = Math.abs(x - point.x);
  20962. var distY = Math.abs(y - point.y);
  20963. var dist = Math.sqrt(distX * distX + distY * distY);
  20964. if ((closestDist === null || dist < closestDist) && dist < distMax) {
  20965. closestDist = dist;
  20966. closestDataPoint = dataPoint;
  20967. }
  20968. }
  20969. }
  20970. }
  20971. return closestDataPoint;
  20972. };
  20973. /**
  20974. * Display a tooltip for given data point
  20975. * @param {Object} dataPoint
  20976. * @private
  20977. */
  20978. Graph3d.prototype._showTooltip = function (dataPoint) {
  20979. var content, line, dot;
  20980. if (!this.tooltip) {
  20981. content = document.createElement('div');
  20982. content.style.position = 'absolute';
  20983. content.style.padding = '10px';
  20984. content.style.border = '1px solid #4d4d4d';
  20985. content.style.color = '#1a1a1a';
  20986. content.style.background = 'rgba(255,255,255,0.7)';
  20987. content.style.borderRadius = '2px';
  20988. content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
  20989. line = document.createElement('div');
  20990. line.style.position = 'absolute';
  20991. line.style.height = '40px';
  20992. line.style.width = '0';
  20993. line.style.borderLeft = '1px solid #4d4d4d';
  20994. dot = document.createElement('div');
  20995. dot.style.position = 'absolute';
  20996. dot.style.height = '0';
  20997. dot.style.width = '0';
  20998. dot.style.border = '5px solid #4d4d4d';
  20999. dot.style.borderRadius = '5px';
  21000. this.tooltip = {
  21001. dataPoint: null,
  21002. dom: {
  21003. content: content,
  21004. line: line,
  21005. dot: dot
  21006. }
  21007. };
  21008. }
  21009. else {
  21010. content = this.tooltip.dom.content;
  21011. line = this.tooltip.dom.line;
  21012. dot = this.tooltip.dom.dot;
  21013. }
  21014. this._hideTooltip();
  21015. this.tooltip.dataPoint = dataPoint;
  21016. if (typeof this.showTooltip === 'function') {
  21017. content.innerHTML = this.showTooltip(dataPoint.point);
  21018. }
  21019. else {
  21020. content.innerHTML = '<table>' +
  21021. '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
  21022. '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
  21023. '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
  21024. '</table>';
  21025. }
  21026. content.style.left = '0';
  21027. content.style.top = '0';
  21028. this.frame.appendChild(content);
  21029. this.frame.appendChild(line);
  21030. this.frame.appendChild(dot);
  21031. // calculate sizes
  21032. var contentWidth = content.offsetWidth;
  21033. var contentHeight = content.offsetHeight;
  21034. var lineHeight = line.offsetHeight;
  21035. var dotWidth = dot.offsetWidth;
  21036. var dotHeight = dot.offsetHeight;
  21037. var left = dataPoint.screen.x - contentWidth / 2;
  21038. left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
  21039. line.style.left = dataPoint.screen.x + 'px';
  21040. line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
  21041. content.style.left = left + 'px';
  21042. content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
  21043. dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
  21044. dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
  21045. };
  21046. /**
  21047. * Hide the tooltip when displayed
  21048. * @private
  21049. */
  21050. Graph3d.prototype._hideTooltip = function () {
  21051. if (this.tooltip) {
  21052. this.tooltip.dataPoint = null;
  21053. for (var prop in this.tooltip.dom) {
  21054. if (this.tooltip.dom.hasOwnProperty(prop)) {
  21055. var elem = this.tooltip.dom[prop];
  21056. if (elem && elem.parentNode) {
  21057. elem.parentNode.removeChild(elem);
  21058. }
  21059. }
  21060. }
  21061. }
  21062. };
  21063. /**
  21064. * Add and event listener. Works for all browsers
  21065. * @param {Element} element An html element
  21066. * @param {string} action The action, for example 'click',
  21067. * without the prefix 'on'
  21068. * @param {function} listener The callback function to be executed
  21069. * @param {boolean} useCapture
  21070. */
  21071. G3DaddEventListener = function(element, action, listener, useCapture) {
  21072. if (element.addEventListener) {
  21073. if (useCapture === undefined)
  21074. useCapture = false;
  21075. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  21076. action = 'DOMMouseScroll'; // For Firefox
  21077. }
  21078. element.addEventListener(action, listener, useCapture);
  21079. } else {
  21080. element.attachEvent('on' + action, listener); // IE browsers
  21081. }
  21082. };
  21083. /**
  21084. * Remove an event listener from an element
  21085. * @param {Element} element An html dom element
  21086. * @param {string} action The name of the event, for example 'mousedown'
  21087. * @param {function} listener The listener function
  21088. * @param {boolean} useCapture
  21089. */
  21090. G3DremoveEventListener = function(element, action, listener, useCapture) {
  21091. if (element.removeEventListener) {
  21092. // non-IE browsers
  21093. if (useCapture === undefined)
  21094. useCapture = false;
  21095. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  21096. action = 'DOMMouseScroll'; // For Firefox
  21097. }
  21098. element.removeEventListener(action, listener, useCapture);
  21099. } else {
  21100. // IE browsers
  21101. element.detachEvent('on' + action, listener);
  21102. }
  21103. };
  21104. /**
  21105. * Stop event propagation
  21106. */
  21107. G3DstopPropagation = function(event) {
  21108. if (!event)
  21109. event = window.event;
  21110. if (event.stopPropagation) {
  21111. event.stopPropagation(); // non-IE browsers
  21112. }
  21113. else {
  21114. event.cancelBubble = true; // IE browsers
  21115. }
  21116. };
  21117. /**
  21118. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  21119. */
  21120. G3DpreventDefault = function (event) {
  21121. if (!event)
  21122. event = window.event;
  21123. if (event.preventDefault) {
  21124. event.preventDefault(); // non-IE browsers
  21125. }
  21126. else {
  21127. event.returnValue = false; // IE browsers
  21128. }
  21129. };
  21130. /**
  21131. * @prototype Point3d
  21132. * @param {Number} x
  21133. * @param {Number} y
  21134. * @param {Number} z
  21135. */
  21136. function Point3d(x, y, z) {
  21137. this.x = x !== undefined ? x : 0;
  21138. this.y = y !== undefined ? y : 0;
  21139. this.z = z !== undefined ? z : 0;
  21140. };
  21141. /**
  21142. * Subtract the two provided points, returns a-b
  21143. * @param {Point3d} a
  21144. * @param {Point3d} b
  21145. * @return {Point3d} a-b
  21146. */
  21147. Point3d.subtract = function(a, b) {
  21148. var sub = new Point3d();
  21149. sub.x = a.x - b.x;
  21150. sub.y = a.y - b.y;
  21151. sub.z = a.z - b.z;
  21152. return sub;
  21153. };
  21154. /**
  21155. * Add the two provided points, returns a+b
  21156. * @param {Point3d} a
  21157. * @param {Point3d} b
  21158. * @return {Point3d} a+b
  21159. */
  21160. Point3d.add = function(a, b) {
  21161. var sum = new Point3d();
  21162. sum.x = a.x + b.x;
  21163. sum.y = a.y + b.y;
  21164. sum.z = a.z + b.z;
  21165. return sum;
  21166. };
  21167. /**
  21168. * Calculate the average of two 3d points
  21169. * @param {Point3d} a
  21170. * @param {Point3d} b
  21171. * @return {Point3d} The average, (a+b)/2
  21172. */
  21173. Point3d.avg = function(a, b) {
  21174. return new Point3d(
  21175. (a.x + b.x) / 2,
  21176. (a.y + b.y) / 2,
  21177. (a.z + b.z) / 2
  21178. );
  21179. };
  21180. /**
  21181. * Calculate the cross product of the two provided points, returns axb
  21182. * Documentation: http://en.wikipedia.org/wiki/Cross_product
  21183. * @param {Point3d} a
  21184. * @param {Point3d} b
  21185. * @return {Point3d} cross product axb
  21186. */
  21187. Point3d.crossProduct = function(a, b) {
  21188. var crossproduct = new Point3d();
  21189. crossproduct.x = a.y * b.z - a.z * b.y;
  21190. crossproduct.y = a.z * b.x - a.x * b.z;
  21191. crossproduct.z = a.x * b.y - a.y * b.x;
  21192. return crossproduct;
  21193. };
  21194. /**
  21195. * Rtrieve the length of the vector (or the distance from this point to the origin
  21196. * @return {Number} length
  21197. */
  21198. Point3d.prototype.length = function() {
  21199. return Math.sqrt(
  21200. this.x * this.x +
  21201. this.y * this.y +
  21202. this.z * this.z
  21203. );
  21204. };
  21205. /**
  21206. * @prototype Point2d
  21207. */
  21208. Point2d = function (x, y) {
  21209. this.x = x !== undefined ? x : 0;
  21210. this.y = y !== undefined ? y : 0;
  21211. };
  21212. /**
  21213. * @class Filter
  21214. *
  21215. * @param {DataSet} data The google data table
  21216. * @param {Number} column The index of the column to be filtered
  21217. * @param {Graph} graph The graph
  21218. */
  21219. function Filter (data, column, graph) {
  21220. this.data = data;
  21221. this.column = column;
  21222. this.graph = graph; // the parent graph
  21223. this.index = undefined;
  21224. this.value = undefined;
  21225. // read all distinct values and select the first one
  21226. this.values = graph.getDistinctValues(data.get(), this.column);
  21227. // sort both numeric and string values correctly
  21228. this.values.sort(function (a, b) {
  21229. return a > b ? 1 : a < b ? -1 : 0;
  21230. });
  21231. if (this.values.length > 0) {
  21232. this.selectValue(0);
  21233. }
  21234. // create an array with the filtered datapoints. this will be loaded afterwards
  21235. this.dataPoints = [];
  21236. this.loaded = false;
  21237. this.onLoadCallback = undefined;
  21238. if (graph.animationPreload) {
  21239. this.loaded = false;
  21240. this.loadInBackground();
  21241. }
  21242. else {
  21243. this.loaded = true;
  21244. }
  21245. };
  21246. /**
  21247. * Return the label
  21248. * @return {string} label
  21249. */
  21250. Filter.prototype.isLoaded = function() {
  21251. return this.loaded;
  21252. };
  21253. /**
  21254. * Return the loaded progress
  21255. * @return {Number} percentage between 0 and 100
  21256. */
  21257. Filter.prototype.getLoadedProgress = function() {
  21258. var len = this.values.length;
  21259. var i = 0;
  21260. while (this.dataPoints[i]) {
  21261. i++;
  21262. }
  21263. return Math.round(i / len * 100);
  21264. };
  21265. /**
  21266. * Return the label
  21267. * @return {string} label
  21268. */
  21269. Filter.prototype.getLabel = function() {
  21270. return this.graph.filterLabel;
  21271. };
  21272. /**
  21273. * Return the columnIndex of the filter
  21274. * @return {Number} columnIndex
  21275. */
  21276. Filter.prototype.getColumn = function() {
  21277. return this.column;
  21278. };
  21279. /**
  21280. * Return the currently selected value. Returns undefined if there is no selection
  21281. * @return {*} value
  21282. */
  21283. Filter.prototype.getSelectedValue = function() {
  21284. if (this.index === undefined)
  21285. return undefined;
  21286. return this.values[this.index];
  21287. };
  21288. /**
  21289. * Retrieve all values of the filter
  21290. * @return {Array} values
  21291. */
  21292. Filter.prototype.getValues = function() {
  21293. return this.values;
  21294. };
  21295. /**
  21296. * Retrieve one value of the filter
  21297. * @param {Number} index
  21298. * @return {*} value
  21299. */
  21300. Filter.prototype.getValue = function(index) {
  21301. if (index >= this.values.length)
  21302. throw 'Error: index out of range';
  21303. return this.values[index];
  21304. };
  21305. /**
  21306. * Retrieve the (filtered) dataPoints for the currently selected filter index
  21307. * @param {Number} [index] (optional)
  21308. * @return {Array} dataPoints
  21309. */
  21310. Filter.prototype._getDataPoints = function(index) {
  21311. if (index === undefined)
  21312. index = this.index;
  21313. if (index === undefined)
  21314. return [];
  21315. var dataPoints;
  21316. if (this.dataPoints[index]) {
  21317. dataPoints = this.dataPoints[index];
  21318. }
  21319. else {
  21320. var f = {};
  21321. f.column = this.column;
  21322. f.value = this.values[index];
  21323. var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
  21324. dataPoints = this.graph._getDataPoints(dataView);
  21325. this.dataPoints[index] = dataPoints;
  21326. }
  21327. return dataPoints;
  21328. };
  21329. /**
  21330. * Set a callback function when the filter is fully loaded.
  21331. */
  21332. Filter.prototype.setOnLoadCallback = function(callback) {
  21333. this.onLoadCallback = callback;
  21334. };
  21335. /**
  21336. * Add a value to the list with available values for this filter
  21337. * No double entries will be created.
  21338. * @param {Number} index
  21339. */
  21340. Filter.prototype.selectValue = function(index) {
  21341. if (index >= this.values.length)
  21342. throw 'Error: index out of range';
  21343. this.index = index;
  21344. this.value = this.values[index];
  21345. };
  21346. /**
  21347. * Load all filtered rows in the background one by one
  21348. * Start this method without providing an index!
  21349. */
  21350. Filter.prototype.loadInBackground = function(index) {
  21351. if (index === undefined)
  21352. index = 0;
  21353. var frame = this.graph.frame;
  21354. if (index < this.values.length) {
  21355. var dataPointsTemp = this._getDataPoints(index);
  21356. //this.graph.redrawInfo(); // TODO: not neat
  21357. // create a progress box
  21358. if (frame.progress === undefined) {
  21359. frame.progress = document.createElement('DIV');
  21360. frame.progress.style.position = 'absolute';
  21361. frame.progress.style.color = 'gray';
  21362. frame.appendChild(frame.progress);
  21363. }
  21364. var progress = this.getLoadedProgress();
  21365. frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
  21366. // TODO: this is no nice solution...
  21367. frame.progress.style.bottom = Graph3d.px(60); // TODO: use height of slider
  21368. frame.progress.style.left = Graph3d.px(10);
  21369. var me = this;
  21370. setTimeout(function() {me.loadInBackground(index+1);}, 10);
  21371. this.loaded = false;
  21372. }
  21373. else {
  21374. this.loaded = true;
  21375. // remove the progress box
  21376. if (frame.progress !== undefined) {
  21377. frame.removeChild(frame.progress);
  21378. frame.progress = undefined;
  21379. }
  21380. if (this.onLoadCallback)
  21381. this.onLoadCallback();
  21382. }
  21383. };
  21384. /**
  21385. * @prototype StepNumber
  21386. * The class StepNumber is an iterator for Numbers. You provide a start and end
  21387. * value, and a best step size. StepNumber itself rounds to fixed values and
  21388. * a finds the step that best fits the provided step.
  21389. *
  21390. * If prettyStep is true, the step size is chosen as close as possible to the
  21391. * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
  21392. *
  21393. * Example usage:
  21394. * var step = new StepNumber(0, 10, 2.5, true);
  21395. * step.start();
  21396. * while (!step.end()) {
  21397. * alert(step.getCurrent());
  21398. * step.next();
  21399. * }
  21400. *
  21401. * Version: 1.0
  21402. *
  21403. * @param {Number} start The start value
  21404. * @param {Number} end The end value
  21405. * @param {Number} step Optional. Step size. Must be a positive value.
  21406. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  21407. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  21408. */
  21409. StepNumber = function (start, end, step, prettyStep) {
  21410. // set default values
  21411. this._start = 0;
  21412. this._end = 0;
  21413. this._step = 1;
  21414. this.prettyStep = true;
  21415. this.precision = 5;
  21416. this._current = 0;
  21417. this.setRange(start, end, step, prettyStep);
  21418. };
  21419. /**
  21420. * Set a new range: start, end and step.
  21421. *
  21422. * @param {Number} start The start value
  21423. * @param {Number} end The end value
  21424. * @param {Number} step Optional. Step size. Must be a positive value.
  21425. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  21426. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  21427. */
  21428. StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
  21429. this._start = start ? start : 0;
  21430. this._end = end ? end : 0;
  21431. this.setStep(step, prettyStep);
  21432. };
  21433. /**
  21434. * Set a new step size
  21435. * @param {Number} step New step size. Must be a positive value
  21436. * @param {boolean} prettyStep Optional. If true, the provided step is rounded
  21437. * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  21438. */
  21439. StepNumber.prototype.setStep = function(step, prettyStep) {
  21440. if (step === undefined || step <= 0)
  21441. return;
  21442. if (prettyStep !== undefined)
  21443. this.prettyStep = prettyStep;
  21444. if (this.prettyStep === true)
  21445. this._step = StepNumber.calculatePrettyStep(step);
  21446. else
  21447. this._step = step;
  21448. };
  21449. /**
  21450. * Calculate a nice step size, closest to the desired step size.
  21451. * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
  21452. * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
  21453. * @param {Number} step Desired step size
  21454. * @return {Number} Nice step size
  21455. */
  21456. StepNumber.calculatePrettyStep = function (step) {
  21457. var log10 = function (x) {return Math.log(x) / Math.LN10;};
  21458. // try three steps (multiple of 1, 2, or 5
  21459. var step1 = Math.pow(10, Math.round(log10(step))),
  21460. step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
  21461. step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
  21462. // choose the best step (closest to minimum step)
  21463. var prettyStep = step1;
  21464. if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
  21465. if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
  21466. // for safety
  21467. if (prettyStep <= 0) {
  21468. prettyStep = 1;
  21469. }
  21470. return prettyStep;
  21471. };
  21472. /**
  21473. * returns the current value of the step
  21474. * @return {Number} current value
  21475. */
  21476. StepNumber.prototype.getCurrent = function () {
  21477. return parseFloat(this._current.toPrecision(this.precision));
  21478. };
  21479. /**
  21480. * returns the current step size
  21481. * @return {Number} current step size
  21482. */
  21483. StepNumber.prototype.getStep = function () {
  21484. return this._step;
  21485. };
  21486. /**
  21487. * Set the current value to the largest value smaller than start, which
  21488. * is a multiple of the step size
  21489. */
  21490. StepNumber.prototype.start = function() {
  21491. this._current = this._start - this._start % this._step;
  21492. };
  21493. /**
  21494. * Do a step, add the step size to the current value
  21495. */
  21496. StepNumber.prototype.next = function () {
  21497. this._current += this._step;
  21498. };
  21499. /**
  21500. * Returns true whether the end is reached
  21501. * @return {boolean} True if the current value has passed the end value.
  21502. */
  21503. StepNumber.prototype.end = function () {
  21504. return (this._current > this._end);
  21505. };
  21506. /**
  21507. * @constructor Slider
  21508. *
  21509. * An html slider control with start/stop/prev/next buttons
  21510. * @param {Element} container The element where the slider will be created
  21511. * @param {Object} options Available options:
  21512. * {boolean} visible If true (default) the
  21513. * slider is visible.
  21514. */
  21515. function Slider(container, options) {
  21516. if (container === undefined) {
  21517. throw 'Error: No container element defined';
  21518. }
  21519. this.container = container;
  21520. this.visible = (options && options.visible != undefined) ? options.visible : true;
  21521. if (this.visible) {
  21522. this.frame = document.createElement('DIV');
  21523. //this.frame.style.backgroundColor = '#E5E5E5';
  21524. this.frame.style.width = '100%';
  21525. this.frame.style.position = 'relative';
  21526. this.container.appendChild(this.frame);
  21527. this.frame.prev = document.createElement('INPUT');
  21528. this.frame.prev.type = 'BUTTON';
  21529. this.frame.prev.value = 'Prev';
  21530. this.frame.appendChild(this.frame.prev);
  21531. this.frame.play = document.createElement('INPUT');
  21532. this.frame.play.type = 'BUTTON';
  21533. this.frame.play.value = 'Play';
  21534. this.frame.appendChild(this.frame.play);
  21535. this.frame.next = document.createElement('INPUT');
  21536. this.frame.next.type = 'BUTTON';
  21537. this.frame.next.value = 'Next';
  21538. this.frame.appendChild(this.frame.next);
  21539. this.frame.bar = document.createElement('INPUT');
  21540. this.frame.bar.type = 'BUTTON';
  21541. this.frame.bar.style.position = 'absolute';
  21542. this.frame.bar.style.border = '1px solid red';
  21543. this.frame.bar.style.width = '100px';
  21544. this.frame.bar.style.height = '6px';
  21545. this.frame.bar.style.borderRadius = '2px';
  21546. this.frame.bar.style.MozBorderRadius = '2px';
  21547. this.frame.bar.style.border = '1px solid #7F7F7F';
  21548. this.frame.bar.style.backgroundColor = '#E5E5E5';
  21549. this.frame.appendChild(this.frame.bar);
  21550. this.frame.slide = document.createElement('INPUT');
  21551. this.frame.slide.type = 'BUTTON';
  21552. this.frame.slide.style.margin = '0px';
  21553. this.frame.slide.value = ' ';
  21554. this.frame.slide.style.position = 'relative';
  21555. this.frame.slide.style.left = '-100px';
  21556. this.frame.appendChild(this.frame.slide);
  21557. // create events
  21558. var me = this;
  21559. this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
  21560. this.frame.prev.onclick = function (event) {me.prev(event);};
  21561. this.frame.play.onclick = function (event) {me.togglePlay(event);};
  21562. this.frame.next.onclick = function (event) {me.next(event);};
  21563. }
  21564. this.onChangeCallback = undefined;
  21565. this.values = [];
  21566. this.index = undefined;
  21567. this.playTimeout = undefined;
  21568. this.playInterval = 1000; // milliseconds
  21569. this.playLoop = true;
  21570. };
  21571. /**
  21572. * Select the previous index
  21573. */
  21574. Slider.prototype.prev = function() {
  21575. var index = this.getIndex();
  21576. if (index > 0) {
  21577. index--;
  21578. this.setIndex(index);
  21579. }
  21580. };
  21581. /**
  21582. * Select the next index
  21583. */
  21584. Slider.prototype.next = function() {
  21585. var index = this.getIndex();
  21586. if (index < this.values.length - 1) {
  21587. index++;
  21588. this.setIndex(index);
  21589. }
  21590. };
  21591. /**
  21592. * Select the next index
  21593. */
  21594. Slider.prototype.playNext = function() {
  21595. var start = new Date();
  21596. var index = this.getIndex();
  21597. if (index < this.values.length - 1) {
  21598. index++;
  21599. this.setIndex(index);
  21600. }
  21601. else if (this.playLoop) {
  21602. // jump to the start
  21603. index = 0;
  21604. this.setIndex(index);
  21605. }
  21606. var end = new Date();
  21607. var diff = (end - start);
  21608. // calculate how much time it to to set the index and to execute the callback
  21609. // function.
  21610. var interval = Math.max(this.playInterval - diff, 0);
  21611. // document.title = diff // TODO: cleanup
  21612. var me = this;
  21613. this.playTimeout = setTimeout(function() {me.playNext();}, interval);
  21614. };
  21615. /**
  21616. * Toggle start or stop playing
  21617. */
  21618. Slider.prototype.togglePlay = function() {
  21619. if (this.playTimeout === undefined) {
  21620. this.play();
  21621. } else {
  21622. this.stop();
  21623. }
  21624. };
  21625. /**
  21626. * Start playing
  21627. */
  21628. Slider.prototype.play = function() {
  21629. // Test whether already playing
  21630. if (this.playTimeout) return;
  21631. this.playNext();
  21632. if (this.frame) {
  21633. this.frame.play.value = 'Stop';
  21634. }
  21635. };
  21636. /**
  21637. * Stop playing
  21638. */
  21639. Slider.prototype.stop = function() {
  21640. clearInterval(this.playTimeout);
  21641. this.playTimeout = undefined;
  21642. if (this.frame) {
  21643. this.frame.play.value = 'Play';
  21644. }
  21645. };
  21646. /**
  21647. * Set a callback function which will be triggered when the value of the
  21648. * slider bar has changed.
  21649. */
  21650. Slider.prototype.setOnChangeCallback = function(callback) {
  21651. this.onChangeCallback = callback;
  21652. };
  21653. /**
  21654. * Set the interval for playing the list
  21655. * @param {Number} interval The interval in milliseconds
  21656. */
  21657. Slider.prototype.setPlayInterval = function(interval) {
  21658. this.playInterval = interval;
  21659. };
  21660. /**
  21661. * Retrieve the current play interval
  21662. * @return {Number} interval The interval in milliseconds
  21663. */
  21664. Slider.prototype.getPlayInterval = function(interval) {
  21665. return this.playInterval;
  21666. };
  21667. /**
  21668. * Set looping on or off
  21669. * @pararm {boolean} doLoop If true, the slider will jump to the start when
  21670. * the end is passed, and will jump to the end
  21671. * when the start is passed.
  21672. */
  21673. Slider.prototype.setPlayLoop = function(doLoop) {
  21674. this.playLoop = doLoop;
  21675. };
  21676. /**
  21677. * Execute the onchange callback function
  21678. */
  21679. Slider.prototype.onChange = function() {
  21680. if (this.onChangeCallback !== undefined) {
  21681. this.onChangeCallback();
  21682. }
  21683. };
  21684. /**
  21685. * redraw the slider on the correct place
  21686. */
  21687. Slider.prototype.redraw = function() {
  21688. if (this.frame) {
  21689. // resize the bar
  21690. this.frame.bar.style.top = (this.frame.clientHeight/2 -
  21691. this.frame.bar.offsetHeight/2) + 'px';
  21692. this.frame.bar.style.width = (this.frame.clientWidth -
  21693. this.frame.prev.clientWidth -
  21694. this.frame.play.clientWidth -
  21695. this.frame.next.clientWidth - 30) + 'px';
  21696. // position the slider button
  21697. var left = this.indexToLeft(this.index);
  21698. this.frame.slide.style.left = (left) + 'px';
  21699. }
  21700. };
  21701. /**
  21702. * Set the list with values for the slider
  21703. * @param {Array} values A javascript array with values (any type)
  21704. */
  21705. Slider.prototype.setValues = function(values) {
  21706. this.values = values;
  21707. if (this.values.length > 0)
  21708. this.setIndex(0);
  21709. else
  21710. this.index = undefined;
  21711. };
  21712. /**
  21713. * Select a value by its index
  21714. * @param {Number} index
  21715. */
  21716. Slider.prototype.setIndex = function(index) {
  21717. if (index < this.values.length) {
  21718. this.index = index;
  21719. this.redraw();
  21720. this.onChange();
  21721. }
  21722. else {
  21723. throw 'Error: index out of range';
  21724. }
  21725. };
  21726. /**
  21727. * retrieve the index of the currently selected vaue
  21728. * @return {Number} index
  21729. */
  21730. Slider.prototype.getIndex = function() {
  21731. return this.index;
  21732. };
  21733. /**
  21734. * retrieve the currently selected value
  21735. * @return {*} value
  21736. */
  21737. Slider.prototype.get = function() {
  21738. return this.values[this.index];
  21739. };
  21740. Slider.prototype._onMouseDown = function(event) {
  21741. // only react on left mouse button down
  21742. var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  21743. if (!leftButtonDown) return;
  21744. this.startClientX = event.clientX;
  21745. this.startSlideX = parseFloat(this.frame.slide.style.left);
  21746. this.frame.style.cursor = 'move';
  21747. // add event listeners to handle moving the contents
  21748. // we store the function onmousemove and onmouseup in the graph, so we can
  21749. // remove the eventlisteners lateron in the function mouseUp()
  21750. var me = this;
  21751. this.onmousemove = function (event) {me._onMouseMove(event);};
  21752. this.onmouseup = function (event) {me._onMouseUp(event);};
  21753. G3DaddEventListener(document, 'mousemove', this.onmousemove);
  21754. G3DaddEventListener(document, 'mouseup', this.onmouseup);
  21755. G3DpreventDefault(event);
  21756. };
  21757. Slider.prototype.leftToIndex = function (left) {
  21758. var width = parseFloat(this.frame.bar.style.width) -
  21759. this.frame.slide.clientWidth - 10;
  21760. var x = left - 3;
  21761. var index = Math.round(x / width * (this.values.length-1));
  21762. if (index < 0) index = 0;
  21763. if (index > this.values.length-1) index = this.values.length-1;
  21764. return index;
  21765. };
  21766. Slider.prototype.indexToLeft = function (index) {
  21767. var width = parseFloat(this.frame.bar.style.width) -
  21768. this.frame.slide.clientWidth - 10;
  21769. var x = index / (this.values.length-1) * width;
  21770. var left = x + 3;
  21771. return left;
  21772. };
  21773. Slider.prototype._onMouseMove = function (event) {
  21774. var diff = event.clientX - this.startClientX;
  21775. var x = this.startSlideX + diff;
  21776. var index = this.leftToIndex(x);
  21777. this.setIndex(index);
  21778. G3DpreventDefault();
  21779. };
  21780. Slider.prototype._onMouseUp = function (event) {
  21781. this.frame.style.cursor = 'auto';
  21782. // remove event listeners
  21783. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  21784. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  21785. G3DpreventDefault();
  21786. };
  21787. /**--------------------------------------------------------------------------**/
  21788. /**
  21789. * Retrieve the absolute left value of a DOM element
  21790. * @param {Element} elem A dom element, for example a div
  21791. * @return {Number} left The absolute left position of this element
  21792. * in the browser page.
  21793. */
  21794. getAbsoluteLeft = function(elem) {
  21795. var left = 0;
  21796. while( elem !== null ) {
  21797. left += elem.offsetLeft;
  21798. left -= elem.scrollLeft;
  21799. elem = elem.offsetParent;
  21800. }
  21801. return left;
  21802. };
  21803. /**
  21804. * Retrieve the absolute top value of a DOM element
  21805. * @param {Element} elem A dom element, for example a div
  21806. * @return {Number} top The absolute top position of this element
  21807. * in the browser page.
  21808. */
  21809. getAbsoluteTop = function(elem) {
  21810. var top = 0;
  21811. while( elem !== null ) {
  21812. top += elem.offsetTop;
  21813. top -= elem.scrollTop;
  21814. elem = elem.offsetParent;
  21815. }
  21816. return top;
  21817. };
  21818. /**
  21819. * Get the horizontal mouse position from a mouse event
  21820. * @param {Event} event
  21821. * @return {Number} mouse x
  21822. */
  21823. getMouseX = function(event) {
  21824. if ('clientX' in event) return event.clientX;
  21825. return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
  21826. };
  21827. /**
  21828. * Get the vertical mouse position from a mouse event
  21829. * @param {Event} event
  21830. * @return {Number} mouse y
  21831. */
  21832. getMouseY = function(event) {
  21833. if ('clientY' in event) return event.clientY;
  21834. return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
  21835. };
  21836. /**
  21837. * vis.js module exports
  21838. */
  21839. var vis = {
  21840. util: util,
  21841. moment: moment,
  21842. DataSet: DataSet,
  21843. DataView: DataView,
  21844. Range: Range,
  21845. stack: stack,
  21846. TimeStep: TimeStep,
  21847. components: {
  21848. items: {
  21849. Item: Item,
  21850. ItemBox: ItemBox,
  21851. ItemPoint: ItemPoint,
  21852. ItemRange: ItemRange
  21853. },
  21854. Component: Component,
  21855. ItemSet: ItemSet,
  21856. TimeAxis: TimeAxis
  21857. },
  21858. network: {
  21859. Node: Node,
  21860. Edge: Edge,
  21861. Popup: Popup,
  21862. Groups: Groups,
  21863. Images: Images
  21864. },
  21865. Timeline: Timeline,
  21866. Network: Network,
  21867. // Deprecated since v3.0.0
  21868. Graph: function () {
  21869. throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
  21870. },
  21871. Graph3d: Graph3d,
  21872. Graph2d: Graph2d
  21873. };
  21874. /**
  21875. * CommonJS module exports
  21876. */
  21877. if (typeof exports !== 'undefined') {
  21878. exports = vis;
  21879. }
  21880. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  21881. module.exports = vis;
  21882. }
  21883. /**
  21884. * AMD module exports
  21885. */
  21886. if (typeof(define) === 'function') {
  21887. define(function () {
  21888. return vis;
  21889. });
  21890. }
  21891. /**
  21892. * Window exports
  21893. */
  21894. if (typeof window !== 'undefined') {
  21895. // attach the module to the window, load as a regular javascript file
  21896. window['vis'] = vis;
  21897. }
  21898. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  21899. /**
  21900. * Expose `Emitter`.
  21901. */
  21902. module.exports = Emitter;
  21903. /**
  21904. * Initialize a new `Emitter`.
  21905. *
  21906. * @api public
  21907. */
  21908. function Emitter(obj) {
  21909. if (obj) return mixin(obj);
  21910. };
  21911. /**
  21912. * Mixin the emitter properties.
  21913. *
  21914. * @param {Object} obj
  21915. * @return {Object}
  21916. * @api private
  21917. */
  21918. function mixin(obj) {
  21919. for (var key in Emitter.prototype) {
  21920. obj[key] = Emitter.prototype[key];
  21921. }
  21922. return obj;
  21923. }
  21924. /**
  21925. * Listen on the given `event` with `fn`.
  21926. *
  21927. * @param {String} event
  21928. * @param {Function} fn
  21929. * @return {Emitter}
  21930. * @api public
  21931. */
  21932. Emitter.prototype.on =
  21933. Emitter.prototype.addEventListener = function(event, fn){
  21934. this._callbacks = this._callbacks || {};
  21935. (this._callbacks[event] = this._callbacks[event] || [])
  21936. .push(fn);
  21937. return this;
  21938. };
  21939. /**
  21940. * Adds an `event` listener that will be invoked a single
  21941. * time then automatically removed.
  21942. *
  21943. * @param {String} event
  21944. * @param {Function} fn
  21945. * @return {Emitter}
  21946. * @api public
  21947. */
  21948. Emitter.prototype.once = function(event, fn){
  21949. var self = this;
  21950. this._callbacks = this._callbacks || {};
  21951. function on() {
  21952. self.off(event, on);
  21953. fn.apply(this, arguments);
  21954. }
  21955. on.fn = fn;
  21956. this.on(event, on);
  21957. return this;
  21958. };
  21959. /**
  21960. * Remove the given callback for `event` or all
  21961. * registered callbacks.
  21962. *
  21963. * @param {String} event
  21964. * @param {Function} fn
  21965. * @return {Emitter}
  21966. * @api public
  21967. */
  21968. Emitter.prototype.off =
  21969. Emitter.prototype.removeListener =
  21970. Emitter.prototype.removeAllListeners =
  21971. Emitter.prototype.removeEventListener = function(event, fn){
  21972. this._callbacks = this._callbacks || {};
  21973. // all
  21974. if (0 == arguments.length) {
  21975. this._callbacks = {};
  21976. return this;
  21977. }
  21978. // specific event
  21979. var callbacks = this._callbacks[event];
  21980. if (!callbacks) return this;
  21981. // remove all handlers
  21982. if (1 == arguments.length) {
  21983. delete this._callbacks[event];
  21984. return this;
  21985. }
  21986. // remove specific handler
  21987. var cb;
  21988. for (var i = 0; i < callbacks.length; i++) {
  21989. cb = callbacks[i];
  21990. if (cb === fn || cb.fn === fn) {
  21991. callbacks.splice(i, 1);
  21992. break;
  21993. }
  21994. }
  21995. return this;
  21996. };
  21997. /**
  21998. * Emit `event` with the given args.
  21999. *
  22000. * @param {String} event
  22001. * @param {Mixed} ...
  22002. * @return {Emitter}
  22003. */
  22004. Emitter.prototype.emit = function(event){
  22005. this._callbacks = this._callbacks || {};
  22006. var args = [].slice.call(arguments, 1)
  22007. , callbacks = this._callbacks[event];
  22008. if (callbacks) {
  22009. callbacks = callbacks.slice(0);
  22010. for (var i = 0, len = callbacks.length; i < len; ++i) {
  22011. callbacks[i].apply(this, args);
  22012. }
  22013. }
  22014. return this;
  22015. };
  22016. /**
  22017. * Return array of callbacks for `event`.
  22018. *
  22019. * @param {String} event
  22020. * @return {Array}
  22021. * @api public
  22022. */
  22023. Emitter.prototype.listeners = function(event){
  22024. this._callbacks = this._callbacks || {};
  22025. return this._callbacks[event] || [];
  22026. };
  22027. /**
  22028. * Check if this emitter has `event` handlers.
  22029. *
  22030. * @param {String} event
  22031. * @return {Boolean}
  22032. * @api public
  22033. */
  22034. Emitter.prototype.hasListeners = function(event){
  22035. return !! this.listeners(event).length;
  22036. };
  22037. },{}],3:[function(require,module,exports){
  22038. /*! Hammer.JS - v1.0.5 - 2013-04-07
  22039. * http://eightmedia.github.com/hammer.js
  22040. *
  22041. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  22042. * Licensed under the MIT license */
  22043. (function(window, undefined) {
  22044. 'use strict';
  22045. /**
  22046. * Hammer
  22047. * use this to create instances
  22048. * @param {HTMLElement} element
  22049. * @param {Object} options
  22050. * @returns {Hammer.Instance}
  22051. * @constructor
  22052. */
  22053. var Hammer = function(element, options) {
  22054. return new Hammer.Instance(element, options || {});
  22055. };
  22056. // default settings
  22057. Hammer.defaults = {
  22058. // add styles and attributes to the element to prevent the browser from doing
  22059. // its native behavior. this doesnt prevent the scrolling, but cancels
  22060. // the contextmenu, tap highlighting etc
  22061. // set to false to disable this
  22062. stop_browser_behavior: {
  22063. // this also triggers onselectstart=false for IE
  22064. userSelect: 'none',
  22065. // this makes the element blocking in IE10 >, you could experiment with the value
  22066. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  22067. touchAction: 'none',
  22068. touchCallout: 'none',
  22069. contentZooming: 'none',
  22070. userDrag: 'none',
  22071. tapHighlightColor: 'rgba(0,0,0,0)'
  22072. }
  22073. // more settings are defined per gesture at gestures.js
  22074. };
  22075. // detect touchevents
  22076. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  22077. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  22078. // dont use mouseevents on mobile devices
  22079. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  22080. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  22081. // eventtypes per touchevent (start, move, end)
  22082. // are filled by Hammer.event.determineEventTypes on setup
  22083. Hammer.EVENT_TYPES = {};
  22084. // direction defines
  22085. Hammer.DIRECTION_DOWN = 'down';
  22086. Hammer.DIRECTION_LEFT = 'left';
  22087. Hammer.DIRECTION_UP = 'up';
  22088. Hammer.DIRECTION_RIGHT = 'right';
  22089. // pointer type
  22090. Hammer.POINTER_MOUSE = 'mouse';
  22091. Hammer.POINTER_TOUCH = 'touch';
  22092. Hammer.POINTER_PEN = 'pen';
  22093. // touch event defines
  22094. Hammer.EVENT_START = 'start';
  22095. Hammer.EVENT_MOVE = 'move';
  22096. Hammer.EVENT_END = 'end';
  22097. // hammer document where the base events are added at
  22098. Hammer.DOCUMENT = document;
  22099. // plugins namespace
  22100. Hammer.plugins = {};
  22101. // if the window events are set...
  22102. Hammer.READY = false;
  22103. /**
  22104. * setup events to detect gestures on the document
  22105. */
  22106. function setup() {
  22107. if(Hammer.READY) {
  22108. return;
  22109. }
  22110. // find what eventtypes we add listeners to
  22111. Hammer.event.determineEventTypes();
  22112. // Register all gestures inside Hammer.gestures
  22113. for(var name in Hammer.gestures) {
  22114. if(Hammer.gestures.hasOwnProperty(name)) {
  22115. Hammer.detection.register(Hammer.gestures[name]);
  22116. }
  22117. }
  22118. // Add touch events on the document
  22119. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  22120. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  22121. // Hammer is ready...!
  22122. Hammer.READY = true;
  22123. }
  22124. /**
  22125. * create new hammer instance
  22126. * all methods should return the instance itself, so it is chainable.
  22127. * @param {HTMLElement} element
  22128. * @param {Object} [options={}]
  22129. * @returns {Hammer.Instance}
  22130. * @constructor
  22131. */
  22132. Hammer.Instance = function(element, options) {
  22133. var self = this;
  22134. // setup HammerJS window events and register all gestures
  22135. // this also sets up the default options
  22136. setup();
  22137. this.element = element;
  22138. // start/stop detection option
  22139. this.enabled = true;
  22140. // merge options
  22141. this.options = Hammer.utils.extend(
  22142. Hammer.utils.extend({}, Hammer.defaults),
  22143. options || {});
  22144. // add some css to the element to prevent the browser from doing its native behavoir
  22145. if(this.options.stop_browser_behavior) {
  22146. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  22147. }
  22148. // start detection on touchstart
  22149. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  22150. if(self.enabled) {
  22151. Hammer.detection.startDetect(self, ev);
  22152. }
  22153. });
  22154. // return instance
  22155. return this;
  22156. };
  22157. Hammer.Instance.prototype = {
  22158. /**
  22159. * bind events to the instance
  22160. * @param {String} gesture
  22161. * @param {Function} handler
  22162. * @returns {Hammer.Instance}
  22163. */
  22164. on: function onEvent(gesture, handler){
  22165. var gestures = gesture.split(' ');
  22166. for(var t=0; t<gestures.length; t++) {
  22167. this.element.addEventListener(gestures[t], handler, false);
  22168. }
  22169. return this;
  22170. },
  22171. /**
  22172. * unbind events to the instance
  22173. * @param {String} gesture
  22174. * @param {Function} handler
  22175. * @returns {Hammer.Instance}
  22176. */
  22177. off: function offEvent(gesture, handler){
  22178. var gestures = gesture.split(' ');
  22179. for(var t=0; t<gestures.length; t++) {
  22180. this.element.removeEventListener(gestures[t], handler, false);
  22181. }
  22182. return this;
  22183. },
  22184. /**
  22185. * trigger gesture event
  22186. * @param {String} gesture
  22187. * @param {Object} eventData
  22188. * @returns {Hammer.Instance}
  22189. */
  22190. trigger: function triggerEvent(gesture, eventData){
  22191. // create DOM event
  22192. var event = Hammer.DOCUMENT.createEvent('Event');
  22193. event.initEvent(gesture, true, true);
  22194. event.gesture = eventData;
  22195. // trigger on the target if it is in the instance element,
  22196. // this is for event delegation tricks
  22197. var element = this.element;
  22198. if(Hammer.utils.hasParent(eventData.target, element)) {
  22199. element = eventData.target;
  22200. }
  22201. element.dispatchEvent(event);
  22202. return this;
  22203. },
  22204. /**
  22205. * enable of disable hammer.js detection
  22206. * @param {Boolean} state
  22207. * @returns {Hammer.Instance}
  22208. */
  22209. enable: function enable(state) {
  22210. this.enabled = state;
  22211. return this;
  22212. }
  22213. };
  22214. /**
  22215. * this holds the last move event,
  22216. * used to fix empty touchend issue
  22217. * see the onTouch event for an explanation
  22218. * @type {Object}
  22219. */
  22220. var last_move_event = null;
  22221. /**
  22222. * when the mouse is hold down, this is true
  22223. * @type {Boolean}
  22224. */
  22225. var enable_detect = false;
  22226. /**
  22227. * when touch events have been fired, this is true
  22228. * @type {Boolean}
  22229. */
  22230. var touch_triggered = false;
  22231. Hammer.event = {
  22232. /**
  22233. * simple addEventListener
  22234. * @param {HTMLElement} element
  22235. * @param {String} type
  22236. * @param {Function} handler
  22237. */
  22238. bindDom: function(element, type, handler) {
  22239. var types = type.split(' ');
  22240. for(var t=0; t<types.length; t++) {
  22241. element.addEventListener(types[t], handler, false);
  22242. }
  22243. },
  22244. /**
  22245. * touch events with mouse fallback
  22246. * @param {HTMLElement} element
  22247. * @param {String} eventType like Hammer.EVENT_MOVE
  22248. * @param {Function} handler
  22249. */
  22250. onTouch: function onTouch(element, eventType, handler) {
  22251. var self = this;
  22252. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  22253. var sourceEventType = ev.type.toLowerCase();
  22254. // onmouseup, but when touchend has been fired we do nothing.
  22255. // this is for touchdevices which also fire a mouseup on touchend
  22256. if(sourceEventType.match(/mouse/) && touch_triggered) {
  22257. return;
  22258. }
  22259. // mousebutton must be down or a touch event
  22260. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  22261. sourceEventType.match(/pointerdown/) || // pointerevents touch
  22262. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  22263. ){
  22264. enable_detect = true;
  22265. }
  22266. // we are in a touch event, set the touch triggered bool to true,
  22267. // this for the conflicts that may occur on ios and android
  22268. if(sourceEventType.match(/touch|pointer/)) {
  22269. touch_triggered = true;
  22270. }
  22271. // count the total touches on the screen
  22272. var count_touches = 0;
  22273. // when touch has been triggered in this detection session
  22274. // and we are now handling a mouse event, we stop that to prevent conflicts
  22275. if(enable_detect) {
  22276. // update pointerevent
  22277. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  22278. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  22279. }
  22280. // touch
  22281. else if(sourceEventType.match(/touch/)) {
  22282. count_touches = ev.touches.length;
  22283. }
  22284. // mouse
  22285. else if(!touch_triggered) {
  22286. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  22287. }
  22288. // if we are in a end event, but when we remove one touch and
  22289. // we still have enough, set eventType to move
  22290. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  22291. eventType = Hammer.EVENT_MOVE;
  22292. }
  22293. // no touches, force the end event
  22294. else if(!count_touches) {
  22295. eventType = Hammer.EVENT_END;
  22296. }
  22297. // because touchend has no touches, and we often want to use these in our gestures,
  22298. // we send the last move event as our eventData in touchend
  22299. if(!count_touches && last_move_event !== null) {
  22300. ev = last_move_event;
  22301. }
  22302. // store the last move event
  22303. else {
  22304. last_move_event = ev;
  22305. }
  22306. // trigger the handler
  22307. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  22308. // remove pointerevent from list
  22309. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  22310. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  22311. }
  22312. }
  22313. //debug(sourceEventType +" "+ eventType);
  22314. // on the end we reset everything
  22315. if(!count_touches) {
  22316. last_move_event = null;
  22317. enable_detect = false;
  22318. touch_triggered = false;
  22319. Hammer.PointerEvent.reset();
  22320. }
  22321. });
  22322. },
  22323. /**
  22324. * we have different events for each device/browser
  22325. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  22326. */
  22327. determineEventTypes: function determineEventTypes() {
  22328. // determine the eventtype we want to set
  22329. var types;
  22330. // pointerEvents magic
  22331. if(Hammer.HAS_POINTEREVENTS) {
  22332. types = Hammer.PointerEvent.getEvents();
  22333. }
  22334. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  22335. else if(Hammer.NO_MOUSEEVENTS) {
  22336. types = [
  22337. 'touchstart',
  22338. 'touchmove',
  22339. 'touchend touchcancel'];
  22340. }
  22341. // for non pointer events browsers and mixed browsers,
  22342. // like chrome on windows8 touch laptop
  22343. else {
  22344. types = [
  22345. 'touchstart mousedown',
  22346. 'touchmove mousemove',
  22347. 'touchend touchcancel mouseup'];
  22348. }
  22349. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  22350. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  22351. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  22352. },
  22353. /**
  22354. * create touchlist depending on the event
  22355. * @param {Object} ev
  22356. * @param {String} eventType used by the fakemultitouch plugin
  22357. */
  22358. getTouchList: function getTouchList(ev/*, eventType*/) {
  22359. // get the fake pointerEvent touchlist
  22360. if(Hammer.HAS_POINTEREVENTS) {
  22361. return Hammer.PointerEvent.getTouchList();
  22362. }
  22363. // get the touchlist
  22364. else if(ev.touches) {
  22365. return ev.touches;
  22366. }
  22367. // make fake touchlist from mouse position
  22368. else {
  22369. return [{
  22370. identifier: 1,
  22371. pageX: ev.pageX,
  22372. pageY: ev.pageY,
  22373. target: ev.target
  22374. }];
  22375. }
  22376. },
  22377. /**
  22378. * collect event data for Hammer js
  22379. * @param {HTMLElement} element
  22380. * @param {String} eventType like Hammer.EVENT_MOVE
  22381. * @param {Object} eventData
  22382. */
  22383. collectEventData: function collectEventData(element, eventType, ev) {
  22384. var touches = this.getTouchList(ev, eventType);
  22385. // find out pointerType
  22386. var pointerType = Hammer.POINTER_TOUCH;
  22387. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  22388. pointerType = Hammer.POINTER_MOUSE;
  22389. }
  22390. return {
  22391. center : Hammer.utils.getCenter(touches),
  22392. timeStamp : new Date().getTime(),
  22393. target : ev.target,
  22394. touches : touches,
  22395. eventType : eventType,
  22396. pointerType : pointerType,
  22397. srcEvent : ev,
  22398. /**
  22399. * prevent the browser default actions
  22400. * mostly used to disable scrolling of the browser
  22401. */
  22402. preventDefault: function() {
  22403. if(this.srcEvent.preventManipulation) {
  22404. this.srcEvent.preventManipulation();
  22405. }
  22406. if(this.srcEvent.preventDefault) {
  22407. this.srcEvent.preventDefault();
  22408. }
  22409. },
  22410. /**
  22411. * stop bubbling the event up to its parents
  22412. */
  22413. stopPropagation: function() {
  22414. this.srcEvent.stopPropagation();
  22415. },
  22416. /**
  22417. * immediately stop gesture detection
  22418. * might be useful after a swipe was detected
  22419. * @return {*}
  22420. */
  22421. stopDetect: function() {
  22422. return Hammer.detection.stopDetect();
  22423. }
  22424. };
  22425. }
  22426. };
  22427. Hammer.PointerEvent = {
  22428. /**
  22429. * holds all pointers
  22430. * @type {Object}
  22431. */
  22432. pointers: {},
  22433. /**
  22434. * get a list of pointers
  22435. * @returns {Array} touchlist
  22436. */
  22437. getTouchList: function() {
  22438. var self = this;
  22439. var touchlist = [];
  22440. // we can use forEach since pointerEvents only is in IE10
  22441. Object.keys(self.pointers).sort().forEach(function(id) {
  22442. touchlist.push(self.pointers[id]);
  22443. });
  22444. return touchlist;
  22445. },
  22446. /**
  22447. * update the position of a pointer
  22448. * @param {String} type Hammer.EVENT_END
  22449. * @param {Object} pointerEvent
  22450. */
  22451. updatePointer: function(type, pointerEvent) {
  22452. if(type == Hammer.EVENT_END) {
  22453. this.pointers = {};
  22454. }
  22455. else {
  22456. pointerEvent.identifier = pointerEvent.pointerId;
  22457. this.pointers[pointerEvent.pointerId] = pointerEvent;
  22458. }
  22459. return Object.keys(this.pointers).length;
  22460. },
  22461. /**
  22462. * check if ev matches pointertype
  22463. * @param {String} pointerType Hammer.POINTER_MOUSE
  22464. * @param {PointerEvent} ev
  22465. */
  22466. matchType: function(pointerType, ev) {
  22467. if(!ev.pointerType) {
  22468. return false;
  22469. }
  22470. var types = {};
  22471. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  22472. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  22473. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  22474. return types[pointerType];
  22475. },
  22476. /**
  22477. * get events
  22478. */
  22479. getEvents: function() {
  22480. return [
  22481. 'pointerdown MSPointerDown',
  22482. 'pointermove MSPointerMove',
  22483. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  22484. ];
  22485. },
  22486. /**
  22487. * reset the list
  22488. */
  22489. reset: function() {
  22490. this.pointers = {};
  22491. }
  22492. };
  22493. Hammer.utils = {
  22494. /**
  22495. * extend method,
  22496. * also used for cloning when dest is an empty object
  22497. * @param {Object} dest
  22498. * @param {Object} src
  22499. * @parm {Boolean} merge do a merge
  22500. * @returns {Object} dest
  22501. */
  22502. extend: function extend(dest, src, merge) {
  22503. for (var key in src) {
  22504. if(dest[key] !== undefined && merge) {
  22505. continue;
  22506. }
  22507. dest[key] = src[key];
  22508. }
  22509. return dest;
  22510. },
  22511. /**
  22512. * find if a node is in the given parent
  22513. * used for event delegation tricks
  22514. * @param {HTMLElement} node
  22515. * @param {HTMLElement} parent
  22516. * @returns {boolean} has_parent
  22517. */
  22518. hasParent: function(node, parent) {
  22519. while(node){
  22520. if(node == parent) {
  22521. return true;
  22522. }
  22523. node = node.parentNode;
  22524. }
  22525. return false;
  22526. },
  22527. /**
  22528. * get the center of all the touches
  22529. * @param {Array} touches
  22530. * @returns {Object} center
  22531. */
  22532. getCenter: function getCenter(touches) {
  22533. var valuesX = [], valuesY = [];
  22534. for(var t= 0,len=touches.length; t<len; t++) {
  22535. valuesX.push(touches[t].pageX);
  22536. valuesY.push(touches[t].pageY);
  22537. }
  22538. return {
  22539. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  22540. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  22541. };
  22542. },
  22543. /**
  22544. * calculate the velocity between two points
  22545. * @param {Number} delta_time
  22546. * @param {Number} delta_x
  22547. * @param {Number} delta_y
  22548. * @returns {Object} velocity
  22549. */
  22550. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  22551. return {
  22552. x: Math.abs(delta_x / delta_time) || 0,
  22553. y: Math.abs(delta_y / delta_time) || 0
  22554. };
  22555. },
  22556. /**
  22557. * calculate the angle between two coordinates
  22558. * @param {Touch} touch1
  22559. * @param {Touch} touch2
  22560. * @returns {Number} angle
  22561. */
  22562. getAngle: function getAngle(touch1, touch2) {
  22563. var y = touch2.pageY - touch1.pageY,
  22564. x = touch2.pageX - touch1.pageX;
  22565. return Math.atan2(y, x) * 180 / Math.PI;
  22566. },
  22567. /**
  22568. * angle to direction define
  22569. * @param {Touch} touch1
  22570. * @param {Touch} touch2
  22571. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  22572. */
  22573. getDirection: function getDirection(touch1, touch2) {
  22574. var x = Math.abs(touch1.pageX - touch2.pageX),
  22575. y = Math.abs(touch1.pageY - touch2.pageY);
  22576. if(x >= y) {
  22577. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  22578. }
  22579. else {
  22580. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  22581. }
  22582. },
  22583. /**
  22584. * calculate the distance between two touches
  22585. * @param {Touch} touch1
  22586. * @param {Touch} touch2
  22587. * @returns {Number} distance
  22588. */
  22589. getDistance: function getDistance(touch1, touch2) {
  22590. var x = touch2.pageX - touch1.pageX,
  22591. y = touch2.pageY - touch1.pageY;
  22592. return Math.sqrt((x*x) + (y*y));
  22593. },
  22594. /**
  22595. * calculate the scale factor between two touchLists (fingers)
  22596. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  22597. * @param {Array} start
  22598. * @param {Array} end
  22599. * @returns {Number} scale
  22600. */
  22601. getScale: function getScale(start, end) {
  22602. // need two fingers...
  22603. if(start.length >= 2 && end.length >= 2) {
  22604. return this.getDistance(end[0], end[1]) /
  22605. this.getDistance(start[0], start[1]);
  22606. }
  22607. return 1;
  22608. },
  22609. /**
  22610. * calculate the rotation degrees between two touchLists (fingers)
  22611. * @param {Array} start
  22612. * @param {Array} end
  22613. * @returns {Number} rotation
  22614. */
  22615. getRotation: function getRotation(start, end) {
  22616. // need two fingers
  22617. if(start.length >= 2 && end.length >= 2) {
  22618. return this.getAngle(end[1], end[0]) -
  22619. this.getAngle(start[1], start[0]);
  22620. }
  22621. return 0;
  22622. },
  22623. /**
  22624. * boolean if the direction is vertical
  22625. * @param {String} direction
  22626. * @returns {Boolean} is_vertical
  22627. */
  22628. isVertical: function isVertical(direction) {
  22629. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  22630. },
  22631. /**
  22632. * stop browser default behavior with css props
  22633. * @param {HtmlElement} element
  22634. * @param {Object} css_props
  22635. */
  22636. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  22637. var prop,
  22638. vendors = ['webkit','khtml','moz','ms','o',''];
  22639. if(!css_props || !element.style) {
  22640. return;
  22641. }
  22642. // with css properties for modern browsers
  22643. for(var i = 0; i < vendors.length; i++) {
  22644. for(var p in css_props) {
  22645. if(css_props.hasOwnProperty(p)) {
  22646. prop = p;
  22647. // vender prefix at the property
  22648. if(vendors[i]) {
  22649. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  22650. }
  22651. // set the style
  22652. element.style[prop] = css_props[p];
  22653. }
  22654. }
  22655. }
  22656. // also the disable onselectstart
  22657. if(css_props.userSelect == 'none') {
  22658. element.onselectstart = function() {
  22659. return false;
  22660. };
  22661. }
  22662. }
  22663. };
  22664. Hammer.detection = {
  22665. // contains all registred Hammer.gestures in the correct order
  22666. gestures: [],
  22667. // data of the current Hammer.gesture detection session
  22668. current: null,
  22669. // the previous Hammer.gesture session data
  22670. // is a full clone of the previous gesture.current object
  22671. previous: null,
  22672. // when this becomes true, no gestures are fired
  22673. stopped: false,
  22674. /**
  22675. * start Hammer.gesture detection
  22676. * @param {Hammer.Instance} inst
  22677. * @param {Object} eventData
  22678. */
  22679. startDetect: function startDetect(inst, eventData) {
  22680. // already busy with a Hammer.gesture detection on an element
  22681. if(this.current) {
  22682. return;
  22683. }
  22684. this.stopped = false;
  22685. this.current = {
  22686. inst : inst, // reference to HammerInstance we're working for
  22687. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  22688. lastEvent : false, // last eventData
  22689. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  22690. };
  22691. this.detect(eventData);
  22692. },
  22693. /**
  22694. * Hammer.gesture detection
  22695. * @param {Object} eventData
  22696. * @param {Object} eventData
  22697. */
  22698. detect: function detect(eventData) {
  22699. if(!this.current || this.stopped) {
  22700. return;
  22701. }
  22702. // extend event data with calculations about scale, distance etc
  22703. eventData = this.extendEventData(eventData);
  22704. // instance options
  22705. var inst_options = this.current.inst.options;
  22706. // call Hammer.gesture handlers
  22707. for(var g=0,len=this.gestures.length; g<len; g++) {
  22708. var gesture = this.gestures[g];
  22709. // only when the instance options have enabled this gesture
  22710. if(!this.stopped && inst_options[gesture.name] !== false) {
  22711. // if a handler returns false, we stop with the detection
  22712. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  22713. this.stopDetect();
  22714. break;
  22715. }
  22716. }
  22717. }
  22718. // store as previous event event
  22719. if(this.current) {
  22720. this.current.lastEvent = eventData;
  22721. }
  22722. // endevent, but not the last touch, so dont stop
  22723. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  22724. this.stopDetect();
  22725. }
  22726. return eventData;
  22727. },
  22728. /**
  22729. * clear the Hammer.gesture vars
  22730. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  22731. * to stop other Hammer.gestures from being fired
  22732. */
  22733. stopDetect: function stopDetect() {
  22734. // clone current data to the store as the previous gesture
  22735. // used for the double tap gesture, since this is an other gesture detect session
  22736. this.previous = Hammer.utils.extend({}, this.current);
  22737. // reset the current
  22738. this.current = null;
  22739. // stopped!
  22740. this.stopped = true;
  22741. },
  22742. /**
  22743. * extend eventData for Hammer.gestures
  22744. * @param {Object} ev
  22745. * @returns {Object} ev
  22746. */
  22747. extendEventData: function extendEventData(ev) {
  22748. var startEv = this.current.startEvent;
  22749. // if the touches change, set the new touches over the startEvent touches
  22750. // this because touchevents don't have all the touches on touchstart, or the
  22751. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  22752. // but, sometimes it happens that both fingers are touching at the EXACT same time
  22753. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  22754. // extend 1 level deep to get the touchlist with the touch objects
  22755. startEv.touches = [];
  22756. for(var i=0,len=ev.touches.length; i<len; i++) {
  22757. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  22758. }
  22759. }
  22760. var delta_time = ev.timeStamp - startEv.timeStamp,
  22761. delta_x = ev.center.pageX - startEv.center.pageX,
  22762. delta_y = ev.center.pageY - startEv.center.pageY,
  22763. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  22764. Hammer.utils.extend(ev, {
  22765. deltaTime : delta_time,
  22766. deltaX : delta_x,
  22767. deltaY : delta_y,
  22768. velocityX : velocity.x,
  22769. velocityY : velocity.y,
  22770. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  22771. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  22772. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  22773. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  22774. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  22775. startEvent : startEv
  22776. });
  22777. return ev;
  22778. },
  22779. /**
  22780. * register new gesture
  22781. * @param {Object} gesture object, see gestures.js for documentation
  22782. * @returns {Array} gestures
  22783. */
  22784. register: function register(gesture) {
  22785. // add an enable gesture options if there is no given
  22786. var options = gesture.defaults || {};
  22787. if(options[gesture.name] === undefined) {
  22788. options[gesture.name] = true;
  22789. }
  22790. // extend Hammer default options with the Hammer.gesture options
  22791. Hammer.utils.extend(Hammer.defaults, options, true);
  22792. // set its index
  22793. gesture.index = gesture.index || 1000;
  22794. // add Hammer.gesture to the list
  22795. this.gestures.push(gesture);
  22796. // sort the list by index
  22797. this.gestures.sort(function(a, b) {
  22798. if (a.index < b.index) {
  22799. return -1;
  22800. }
  22801. if (a.index > b.index) {
  22802. return 1;
  22803. }
  22804. return 0;
  22805. });
  22806. return this.gestures;
  22807. }
  22808. };
  22809. Hammer.gestures = Hammer.gestures || {};
  22810. /**
  22811. * Custom gestures
  22812. * ==============================
  22813. *
  22814. * Gesture object
  22815. * --------------------
  22816. * The object structure of a gesture:
  22817. *
  22818. * { name: 'mygesture',
  22819. * index: 1337,
  22820. * defaults: {
  22821. * mygesture_option: true
  22822. * }
  22823. * handler: function(type, ev, inst) {
  22824. * // trigger gesture event
  22825. * inst.trigger(this.name, ev);
  22826. * }
  22827. * }
  22828. * @param {String} name
  22829. * this should be the name of the gesture, lowercase
  22830. * it is also being used to disable/enable the gesture per instance config.
  22831. *
  22832. * @param {Number} [index=1000]
  22833. * the index of the gesture, where it is going to be in the stack of gestures detection
  22834. * like when you build an gesture that depends on the drag gesture, it is a good
  22835. * idea to place it after the index of the drag gesture.
  22836. *
  22837. * @param {Object} [defaults={}]
  22838. * the default settings of the gesture. these are added to the instance settings,
  22839. * and can be overruled per instance. you can also add the name of the gesture,
  22840. * but this is also added by default (and set to true).
  22841. *
  22842. * @param {Function} handler
  22843. * this handles the gesture detection of your custom gesture and receives the
  22844. * following arguments:
  22845. *
  22846. * @param {Object} eventData
  22847. * event data containing the following properties:
  22848. * timeStamp {Number} time the event occurred
  22849. * target {HTMLElement} target element
  22850. * touches {Array} touches (fingers, pointers, mouse) on the screen
  22851. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  22852. * center {Object} center position of the touches. contains pageX and pageY
  22853. * deltaTime {Number} the total time of the touches in the screen
  22854. * deltaX {Number} the delta on x axis we haved moved
  22855. * deltaY {Number} the delta on y axis we haved moved
  22856. * velocityX {Number} the velocity on the x
  22857. * velocityY {Number} the velocity on y
  22858. * angle {Number} the angle we are moving
  22859. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  22860. * distance {Number} the distance we haved moved
  22861. * scale {Number} scaling of the touches, needs 2 touches
  22862. * rotation {Number} rotation of the touches, needs 2 touches *
  22863. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  22864. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  22865. * startEvent {Object} contains the same properties as above,
  22866. * but from the first touch. this is used to calculate
  22867. * distances, deltaTime, scaling etc
  22868. *
  22869. * @param {Hammer.Instance} inst
  22870. * the instance we are doing the detection for. you can get the options from
  22871. * the inst.options object and trigger the gesture event by calling inst.trigger
  22872. *
  22873. *
  22874. * Handle gestures
  22875. * --------------------
  22876. * inside the handler you can get/set Hammer.detection.current. This is the current
  22877. * detection session. It has the following properties
  22878. * @param {String} name
  22879. * contains the name of the gesture we have detected. it has not a real function,
  22880. * only to check in other gestures if something is detected.
  22881. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  22882. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  22883. *
  22884. * @readonly
  22885. * @param {Hammer.Instance} inst
  22886. * the instance we do the detection for
  22887. *
  22888. * @readonly
  22889. * @param {Object} startEvent
  22890. * contains the properties of the first gesture detection in this session.
  22891. * Used for calculations about timing, distance, etc.
  22892. *
  22893. * @readonly
  22894. * @param {Object} lastEvent
  22895. * contains all the properties of the last gesture detect in this session.
  22896. *
  22897. * after the gesture detection session has been completed (user has released the screen)
  22898. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  22899. * this is usefull for gestures like doubletap, where you need to know if the
  22900. * previous gesture was a tap
  22901. *
  22902. * options that have been set by the instance can be received by calling inst.options
  22903. *
  22904. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  22905. * The first param is the name of your gesture, the second the event argument
  22906. *
  22907. *
  22908. * Register gestures
  22909. * --------------------
  22910. * When an gesture is added to the Hammer.gestures object, it is auto registered
  22911. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  22912. * manually and pass your gesture object as a param
  22913. *
  22914. */
  22915. /**
  22916. * Hold
  22917. * Touch stays at the same place for x time
  22918. * @events hold
  22919. */
  22920. Hammer.gestures.Hold = {
  22921. name: 'hold',
  22922. index: 10,
  22923. defaults: {
  22924. hold_timeout : 500,
  22925. hold_threshold : 1
  22926. },
  22927. timer: null,
  22928. handler: function holdGesture(ev, inst) {
  22929. switch(ev.eventType) {
  22930. case Hammer.EVENT_START:
  22931. // clear any running timers
  22932. clearTimeout(this.timer);
  22933. // set the gesture so we can check in the timeout if it still is
  22934. Hammer.detection.current.name = this.name;
  22935. // set timer and if after the timeout it still is hold,
  22936. // we trigger the hold event
  22937. this.timer = setTimeout(function() {
  22938. if(Hammer.detection.current.name == 'hold') {
  22939. inst.trigger('hold', ev);
  22940. }
  22941. }, inst.options.hold_timeout);
  22942. break;
  22943. // when you move or end we clear the timer
  22944. case Hammer.EVENT_MOVE:
  22945. if(ev.distance > inst.options.hold_threshold) {
  22946. clearTimeout(this.timer);
  22947. }
  22948. break;
  22949. case Hammer.EVENT_END:
  22950. clearTimeout(this.timer);
  22951. break;
  22952. }
  22953. }
  22954. };
  22955. /**
  22956. * Tap/DoubleTap
  22957. * Quick touch at a place or double at the same place
  22958. * @events tap, doubletap
  22959. */
  22960. Hammer.gestures.Tap = {
  22961. name: 'tap',
  22962. index: 100,
  22963. defaults: {
  22964. tap_max_touchtime : 250,
  22965. tap_max_distance : 10,
  22966. tap_always : true,
  22967. doubletap_distance : 20,
  22968. doubletap_interval : 300
  22969. },
  22970. handler: function tapGesture(ev, inst) {
  22971. if(ev.eventType == Hammer.EVENT_END) {
  22972. // previous gesture, for the double tap since these are two different gesture detections
  22973. var prev = Hammer.detection.previous,
  22974. did_doubletap = false;
  22975. // when the touchtime is higher then the max touch time
  22976. // or when the moving distance is too much
  22977. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  22978. ev.distance > inst.options.tap_max_distance) {
  22979. return;
  22980. }
  22981. // check if double tap
  22982. if(prev && prev.name == 'tap' &&
  22983. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  22984. ev.distance < inst.options.doubletap_distance) {
  22985. inst.trigger('doubletap', ev);
  22986. did_doubletap = true;
  22987. }
  22988. // do a single tap
  22989. if(!did_doubletap || inst.options.tap_always) {
  22990. Hammer.detection.current.name = 'tap';
  22991. inst.trigger(Hammer.detection.current.name, ev);
  22992. }
  22993. }
  22994. }
  22995. };
  22996. /**
  22997. * Swipe
  22998. * triggers swipe events when the end velocity is above the threshold
  22999. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  23000. */
  23001. Hammer.gestures.Swipe = {
  23002. name: 'swipe',
  23003. index: 40,
  23004. defaults: {
  23005. // set 0 for unlimited, but this can conflict with transform
  23006. swipe_max_touches : 1,
  23007. swipe_velocity : 0.7
  23008. },
  23009. handler: function swipeGesture(ev, inst) {
  23010. if(ev.eventType == Hammer.EVENT_END) {
  23011. // max touches
  23012. if(inst.options.swipe_max_touches > 0 &&
  23013. ev.touches.length > inst.options.swipe_max_touches) {
  23014. return;
  23015. }
  23016. // when the distance we moved is too small we skip this gesture
  23017. // or we can be already in dragging
  23018. if(ev.velocityX > inst.options.swipe_velocity ||
  23019. ev.velocityY > inst.options.swipe_velocity) {
  23020. // trigger swipe events
  23021. inst.trigger(this.name, ev);
  23022. inst.trigger(this.name + ev.direction, ev);
  23023. }
  23024. }
  23025. }
  23026. };
  23027. /**
  23028. * Drag
  23029. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  23030. * moving left and right is a good practice. When all the drag events are blocking
  23031. * you disable scrolling on that area.
  23032. * @events drag, drapleft, dragright, dragup, dragdown
  23033. */
  23034. Hammer.gestures.Drag = {
  23035. name: 'drag',
  23036. index: 50,
  23037. defaults: {
  23038. drag_min_distance : 10,
  23039. // set 0 for unlimited, but this can conflict with transform
  23040. drag_max_touches : 1,
  23041. // prevent default browser behavior when dragging occurs
  23042. // be careful with it, it makes the element a blocking element
  23043. // when you are using the drag gesture, it is a good practice to set this true
  23044. drag_block_horizontal : false,
  23045. drag_block_vertical : false,
  23046. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  23047. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  23048. drag_lock_to_axis : false,
  23049. // drag lock only kicks in when distance > drag_lock_min_distance
  23050. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  23051. drag_lock_min_distance : 25
  23052. },
  23053. triggered: false,
  23054. handler: function dragGesture(ev, inst) {
  23055. // current gesture isnt drag, but dragged is true
  23056. // this means an other gesture is busy. now call dragend
  23057. if(Hammer.detection.current.name != this.name && this.triggered) {
  23058. inst.trigger(this.name +'end', ev);
  23059. this.triggered = false;
  23060. return;
  23061. }
  23062. // max touches
  23063. if(inst.options.drag_max_touches > 0 &&
  23064. ev.touches.length > inst.options.drag_max_touches) {
  23065. return;
  23066. }
  23067. switch(ev.eventType) {
  23068. case Hammer.EVENT_START:
  23069. this.triggered = false;
  23070. break;
  23071. case Hammer.EVENT_MOVE:
  23072. // when the distance we moved is too small we skip this gesture
  23073. // or we can be already in dragging
  23074. if(ev.distance < inst.options.drag_min_distance &&
  23075. Hammer.detection.current.name != this.name) {
  23076. return;
  23077. }
  23078. // we are dragging!
  23079. Hammer.detection.current.name = this.name;
  23080. // lock drag to axis?
  23081. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  23082. ev.drag_locked_to_axis = true;
  23083. }
  23084. var last_direction = Hammer.detection.current.lastEvent.direction;
  23085. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  23086. // keep direction on the axis that the drag gesture started on
  23087. if(Hammer.utils.isVertical(last_direction)) {
  23088. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  23089. }
  23090. else {
  23091. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  23092. }
  23093. }
  23094. // first time, trigger dragstart event
  23095. if(!this.triggered) {
  23096. inst.trigger(this.name +'start', ev);
  23097. this.triggered = true;
  23098. }
  23099. // trigger normal event
  23100. inst.trigger(this.name, ev);
  23101. // direction event, like dragdown
  23102. inst.trigger(this.name + ev.direction, ev);
  23103. // block the browser events
  23104. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  23105. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  23106. ev.preventDefault();
  23107. }
  23108. break;
  23109. case Hammer.EVENT_END:
  23110. // trigger dragend
  23111. if(this.triggered) {
  23112. inst.trigger(this.name +'end', ev);
  23113. }
  23114. this.triggered = false;
  23115. break;
  23116. }
  23117. }
  23118. };
  23119. /**
  23120. * Transform
  23121. * User want to scale or rotate with 2 fingers
  23122. * @events transform, pinch, pinchin, pinchout, rotate
  23123. */
  23124. Hammer.gestures.Transform = {
  23125. name: 'transform',
  23126. index: 45,
  23127. defaults: {
  23128. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  23129. transform_min_scale : 0.01,
  23130. // rotation in degrees
  23131. transform_min_rotation : 1,
  23132. // prevent default browser behavior when two touches are on the screen
  23133. // but it makes the element a blocking element
  23134. // when you are using the transform gesture, it is a good practice to set this true
  23135. transform_always_block : false
  23136. },
  23137. triggered: false,
  23138. handler: function transformGesture(ev, inst) {
  23139. // current gesture isnt drag, but dragged is true
  23140. // this means an other gesture is busy. now call dragend
  23141. if(Hammer.detection.current.name != this.name && this.triggered) {
  23142. inst.trigger(this.name +'end', ev);
  23143. this.triggered = false;
  23144. return;
  23145. }
  23146. // atleast multitouch
  23147. if(ev.touches.length < 2) {
  23148. return;
  23149. }
  23150. // prevent default when two fingers are on the screen
  23151. if(inst.options.transform_always_block) {
  23152. ev.preventDefault();
  23153. }
  23154. switch(ev.eventType) {
  23155. case Hammer.EVENT_START:
  23156. this.triggered = false;
  23157. break;
  23158. case Hammer.EVENT_MOVE:
  23159. var scale_threshold = Math.abs(1-ev.scale);
  23160. var rotation_threshold = Math.abs(ev.rotation);
  23161. // when the distance we moved is too small we skip this gesture
  23162. // or we can be already in dragging
  23163. if(scale_threshold < inst.options.transform_min_scale &&
  23164. rotation_threshold < inst.options.transform_min_rotation) {
  23165. return;
  23166. }
  23167. // we are transforming!
  23168. Hammer.detection.current.name = this.name;
  23169. // first time, trigger dragstart event
  23170. if(!this.triggered) {
  23171. inst.trigger(this.name +'start', ev);
  23172. this.triggered = true;
  23173. }
  23174. inst.trigger(this.name, ev); // basic transform event
  23175. // trigger rotate event
  23176. if(rotation_threshold > inst.options.transform_min_rotation) {
  23177. inst.trigger('rotate', ev);
  23178. }
  23179. // trigger pinch event
  23180. if(scale_threshold > inst.options.transform_min_scale) {
  23181. inst.trigger('pinch', ev);
  23182. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  23183. }
  23184. break;
  23185. case Hammer.EVENT_END:
  23186. // trigger dragend
  23187. if(this.triggered) {
  23188. inst.trigger(this.name +'end', ev);
  23189. }
  23190. this.triggered = false;
  23191. break;
  23192. }
  23193. }
  23194. };
  23195. /**
  23196. * Touch
  23197. * Called as first, tells the user has touched the screen
  23198. * @events touch
  23199. */
  23200. Hammer.gestures.Touch = {
  23201. name: 'touch',
  23202. index: -Infinity,
  23203. defaults: {
  23204. // call preventDefault at touchstart, and makes the element blocking by
  23205. // disabling the scrolling of the page, but it improves gestures like
  23206. // transforming and dragging.
  23207. // be careful with using this, it can be very annoying for users to be stuck
  23208. // on the page
  23209. prevent_default: false,
  23210. // disable mouse events, so only touch (or pen!) input triggers events
  23211. prevent_mouseevents: false
  23212. },
  23213. handler: function touchGesture(ev, inst) {
  23214. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  23215. ev.stopDetect();
  23216. return;
  23217. }
  23218. if(inst.options.prevent_default) {
  23219. ev.preventDefault();
  23220. }
  23221. if(ev.eventType == Hammer.EVENT_START) {
  23222. inst.trigger(this.name, ev);
  23223. }
  23224. }
  23225. };
  23226. /**
  23227. * Release
  23228. * Called as last, tells the user has released the screen
  23229. * @events release
  23230. */
  23231. Hammer.gestures.Release = {
  23232. name: 'release',
  23233. index: Infinity,
  23234. handler: function releaseGesture(ev, inst) {
  23235. if(ev.eventType == Hammer.EVENT_END) {
  23236. inst.trigger(this.name, ev);
  23237. }
  23238. }
  23239. };
  23240. // node export
  23241. if(typeof module === 'object' && typeof module.exports === 'object'){
  23242. module.exports = Hammer;
  23243. }
  23244. // just window export
  23245. else {
  23246. window.Hammer = Hammer;
  23247. // requireJS module definition
  23248. if(typeof window.define === 'function' && window.define.amd) {
  23249. window.define('hammer', [], function() {
  23250. return Hammer;
  23251. });
  23252. }
  23253. }
  23254. })(this);
  23255. },{}],4:[function(require,module,exports){
  23256. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  23257. //! version : 2.7.0
  23258. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  23259. //! license : MIT
  23260. //! momentjs.com
  23261. (function (undefined) {
  23262. /************************************
  23263. Constants
  23264. ************************************/
  23265. var moment,
  23266. VERSION = "2.7.0",
  23267. // the global-scope this is NOT the global object in Node.js
  23268. globalScope = typeof global !== 'undefined' ? global : this,
  23269. oldGlobalMoment,
  23270. round = Math.round,
  23271. i,
  23272. YEAR = 0,
  23273. MONTH = 1,
  23274. DATE = 2,
  23275. HOUR = 3,
  23276. MINUTE = 4,
  23277. SECOND = 5,
  23278. MILLISECOND = 6,
  23279. // internal storage for language config files
  23280. languages = {},
  23281. // moment internal properties
  23282. momentProperties = {
  23283. _isAMomentObject: null,
  23284. _i : null,
  23285. _f : null,
  23286. _l : null,
  23287. _strict : null,
  23288. _tzm : null,
  23289. _isUTC : null,
  23290. _offset : null, // optional. Combine with _isUTC
  23291. _pf : null,
  23292. _lang : null // optional
  23293. },
  23294. // check for nodeJS
  23295. hasModule = (typeof module !== 'undefined' && module.exports),
  23296. // ASP.NET json date format regex
  23297. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  23298. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  23299. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  23300. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  23301. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  23302. // format tokens
  23303. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  23304. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  23305. // parsing token regexes
  23306. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  23307. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  23308. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  23309. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  23310. parseTokenDigits = /\d+/, // nonzero number of digits
  23311. parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
  23312. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  23313. parseTokenT = /T/i, // T (ISO separator)
  23314. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  23315. parseTokenOrdinal = /\d{1,2}/,
  23316. //strict parsing regexes
  23317. parseTokenOneDigit = /\d/, // 0 - 9
  23318. parseTokenTwoDigits = /\d\d/, // 00 - 99
  23319. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  23320. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  23321. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  23322. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  23323. // iso 8601 regex
  23324. // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
  23325. isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
  23326. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  23327. isoDates = [
  23328. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  23329. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  23330. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  23331. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  23332. ['YYYY-DDD', /\d{4}-\d{3}/]
  23333. ],
  23334. // iso time formats and regexes
  23335. isoTimes = [
  23336. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  23337. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  23338. ['HH:mm', /(T| )\d\d:\d\d/],
  23339. ['HH', /(T| )\d\d/]
  23340. ],
  23341. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  23342. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  23343. // getter and setter names
  23344. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  23345. unitMillisecondFactors = {
  23346. 'Milliseconds' : 1,
  23347. 'Seconds' : 1e3,
  23348. 'Minutes' : 6e4,
  23349. 'Hours' : 36e5,
  23350. 'Days' : 864e5,
  23351. 'Months' : 2592e6,
  23352. 'Years' : 31536e6
  23353. },
  23354. unitAliases = {
  23355. ms : 'millisecond',
  23356. s : 'second',
  23357. m : 'minute',
  23358. h : 'hour',
  23359. d : 'day',
  23360. D : 'date',
  23361. w : 'week',
  23362. W : 'isoWeek',
  23363. M : 'month',
  23364. Q : 'quarter',
  23365. y : 'year',
  23366. DDD : 'dayOfYear',
  23367. e : 'weekday',
  23368. E : 'isoWeekday',
  23369. gg: 'weekYear',
  23370. GG: 'isoWeekYear'
  23371. },
  23372. camelFunctions = {
  23373. dayofyear : 'dayOfYear',
  23374. isoweekday : 'isoWeekday',
  23375. isoweek : 'isoWeek',
  23376. weekyear : 'weekYear',
  23377. isoweekyear : 'isoWeekYear'
  23378. },
  23379. // format function strings
  23380. formatFunctions = {},
  23381. // default relative time thresholds
  23382. relativeTimeThresholds = {
  23383. s: 45, //seconds to minutes
  23384. m: 45, //minutes to hours
  23385. h: 22, //hours to days
  23386. dd: 25, //days to month (month == 1)
  23387. dm: 45, //days to months (months > 1)
  23388. dy: 345 //days to year
  23389. },
  23390. // tokens to ordinalize and pad
  23391. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  23392. paddedTokens = 'M D H h m s w W'.split(' '),
  23393. formatTokenFunctions = {
  23394. M : function () {
  23395. return this.month() + 1;
  23396. },
  23397. MMM : function (format) {
  23398. return this.lang().monthsShort(this, format);
  23399. },
  23400. MMMM : function (format) {
  23401. return this.lang().months(this, format);
  23402. },
  23403. D : function () {
  23404. return this.date();
  23405. },
  23406. DDD : function () {
  23407. return this.dayOfYear();
  23408. },
  23409. d : function () {
  23410. return this.day();
  23411. },
  23412. dd : function (format) {
  23413. return this.lang().weekdaysMin(this, format);
  23414. },
  23415. ddd : function (format) {
  23416. return this.lang().weekdaysShort(this, format);
  23417. },
  23418. dddd : function (format) {
  23419. return this.lang().weekdays(this, format);
  23420. },
  23421. w : function () {
  23422. return this.week();
  23423. },
  23424. W : function () {
  23425. return this.isoWeek();
  23426. },
  23427. YY : function () {
  23428. return leftZeroFill(this.year() % 100, 2);
  23429. },
  23430. YYYY : function () {
  23431. return leftZeroFill(this.year(), 4);
  23432. },
  23433. YYYYY : function () {
  23434. return leftZeroFill(this.year(), 5);
  23435. },
  23436. YYYYYY : function () {
  23437. var y = this.year(), sign = y >= 0 ? '+' : '-';
  23438. return sign + leftZeroFill(Math.abs(y), 6);
  23439. },
  23440. gg : function () {
  23441. return leftZeroFill(this.weekYear() % 100, 2);
  23442. },
  23443. gggg : function () {
  23444. return leftZeroFill(this.weekYear(), 4);
  23445. },
  23446. ggggg : function () {
  23447. return leftZeroFill(this.weekYear(), 5);
  23448. },
  23449. GG : function () {
  23450. return leftZeroFill(this.isoWeekYear() % 100, 2);
  23451. },
  23452. GGGG : function () {
  23453. return leftZeroFill(this.isoWeekYear(), 4);
  23454. },
  23455. GGGGG : function () {
  23456. return leftZeroFill(this.isoWeekYear(), 5);
  23457. },
  23458. e : function () {
  23459. return this.weekday();
  23460. },
  23461. E : function () {
  23462. return this.isoWeekday();
  23463. },
  23464. a : function () {
  23465. return this.lang().meridiem(this.hours(), this.minutes(), true);
  23466. },
  23467. A : function () {
  23468. return this.lang().meridiem(this.hours(), this.minutes(), false);
  23469. },
  23470. H : function () {
  23471. return this.hours();
  23472. },
  23473. h : function () {
  23474. return this.hours() % 12 || 12;
  23475. },
  23476. m : function () {
  23477. return this.minutes();
  23478. },
  23479. s : function () {
  23480. return this.seconds();
  23481. },
  23482. S : function () {
  23483. return toInt(this.milliseconds() / 100);
  23484. },
  23485. SS : function () {
  23486. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  23487. },
  23488. SSS : function () {
  23489. return leftZeroFill(this.milliseconds(), 3);
  23490. },
  23491. SSSS : function () {
  23492. return leftZeroFill(this.milliseconds(), 3);
  23493. },
  23494. Z : function () {
  23495. var a = -this.zone(),
  23496. b = "+";
  23497. if (a < 0) {
  23498. a = -a;
  23499. b = "-";
  23500. }
  23501. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  23502. },
  23503. ZZ : function () {
  23504. var a = -this.zone(),
  23505. b = "+";
  23506. if (a < 0) {
  23507. a = -a;
  23508. b = "-";
  23509. }
  23510. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  23511. },
  23512. z : function () {
  23513. return this.zoneAbbr();
  23514. },
  23515. zz : function () {
  23516. return this.zoneName();
  23517. },
  23518. X : function () {
  23519. return this.unix();
  23520. },
  23521. Q : function () {
  23522. return this.quarter();
  23523. }
  23524. },
  23525. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  23526. // Pick the first defined of two or three arguments. dfl comes from
  23527. // default.
  23528. function dfl(a, b, c) {
  23529. switch (arguments.length) {
  23530. case 2: return a != null ? a : b;
  23531. case 3: return a != null ? a : b != null ? b : c;
  23532. default: throw new Error("Implement me");
  23533. }
  23534. }
  23535. function defaultParsingFlags() {
  23536. // We need to deep clone this object, and es5 standard is not very
  23537. // helpful.
  23538. return {
  23539. empty : false,
  23540. unusedTokens : [],
  23541. unusedInput : [],
  23542. overflow : -2,
  23543. charsLeftOver : 0,
  23544. nullInput : false,
  23545. invalidMonth : null,
  23546. invalidFormat : false,
  23547. userInvalidated : false,
  23548. iso: false
  23549. };
  23550. }
  23551. function deprecate(msg, fn) {
  23552. var firstTime = true;
  23553. function printMsg() {
  23554. if (moment.suppressDeprecationWarnings === false &&
  23555. typeof console !== 'undefined' && console.warn) {
  23556. console.warn("Deprecation warning: " + msg);
  23557. }
  23558. }
  23559. return extend(function () {
  23560. if (firstTime) {
  23561. printMsg();
  23562. firstTime = false;
  23563. }
  23564. return fn.apply(this, arguments);
  23565. }, fn);
  23566. }
  23567. function padToken(func, count) {
  23568. return function (a) {
  23569. return leftZeroFill(func.call(this, a), count);
  23570. };
  23571. }
  23572. function ordinalizeToken(func, period) {
  23573. return function (a) {
  23574. return this.lang().ordinal(func.call(this, a), period);
  23575. };
  23576. }
  23577. while (ordinalizeTokens.length) {
  23578. i = ordinalizeTokens.pop();
  23579. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  23580. }
  23581. while (paddedTokens.length) {
  23582. i = paddedTokens.pop();
  23583. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  23584. }
  23585. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  23586. /************************************
  23587. Constructors
  23588. ************************************/
  23589. function Language() {
  23590. }
  23591. // Moment prototype object
  23592. function Moment(config) {
  23593. checkOverflow(config);
  23594. extend(this, config);
  23595. }
  23596. // Duration Constructor
  23597. function Duration(duration) {
  23598. var normalizedInput = normalizeObjectUnits(duration),
  23599. years = normalizedInput.year || 0,
  23600. quarters = normalizedInput.quarter || 0,
  23601. months = normalizedInput.month || 0,
  23602. weeks = normalizedInput.week || 0,
  23603. days = normalizedInput.day || 0,
  23604. hours = normalizedInput.hour || 0,
  23605. minutes = normalizedInput.minute || 0,
  23606. seconds = normalizedInput.second || 0,
  23607. milliseconds = normalizedInput.millisecond || 0;
  23608. // representation for dateAddRemove
  23609. this._milliseconds = +milliseconds +
  23610. seconds * 1e3 + // 1000
  23611. minutes * 6e4 + // 1000 * 60
  23612. hours * 36e5; // 1000 * 60 * 60
  23613. // Because of dateAddRemove treats 24 hours as different from a
  23614. // day when working around DST, we need to store them separately
  23615. this._days = +days +
  23616. weeks * 7;
  23617. // It is impossible translate months into days without knowing
  23618. // which months you are are talking about, so we have to store
  23619. // it separately.
  23620. this._months = +months +
  23621. quarters * 3 +
  23622. years * 12;
  23623. this._data = {};
  23624. this._bubble();
  23625. }
  23626. /************************************
  23627. Helpers
  23628. ************************************/
  23629. function extend(a, b) {
  23630. for (var i in b) {
  23631. if (b.hasOwnProperty(i)) {
  23632. a[i] = b[i];
  23633. }
  23634. }
  23635. if (b.hasOwnProperty("toString")) {
  23636. a.toString = b.toString;
  23637. }
  23638. if (b.hasOwnProperty("valueOf")) {
  23639. a.valueOf = b.valueOf;
  23640. }
  23641. return a;
  23642. }
  23643. function cloneMoment(m) {
  23644. var result = {}, i;
  23645. for (i in m) {
  23646. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  23647. result[i] = m[i];
  23648. }
  23649. }
  23650. return result;
  23651. }
  23652. function absRound(number) {
  23653. if (number < 0) {
  23654. return Math.ceil(number);
  23655. } else {
  23656. return Math.floor(number);
  23657. }
  23658. }
  23659. // left zero fill a number
  23660. // see http://jsperf.com/left-zero-filling for performance comparison
  23661. function leftZeroFill(number, targetLength, forceSign) {
  23662. var output = '' + Math.abs(number),
  23663. sign = number >= 0;
  23664. while (output.length < targetLength) {
  23665. output = '0' + output;
  23666. }
  23667. return (sign ? (forceSign ? '+' : '') : '-') + output;
  23668. }
  23669. // helper function for _.addTime and _.subtractTime
  23670. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  23671. var milliseconds = duration._milliseconds,
  23672. days = duration._days,
  23673. months = duration._months;
  23674. updateOffset = updateOffset == null ? true : updateOffset;
  23675. if (milliseconds) {
  23676. mom._d.setTime(+mom._d + milliseconds * isAdding);
  23677. }
  23678. if (days) {
  23679. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  23680. }
  23681. if (months) {
  23682. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  23683. }
  23684. if (updateOffset) {
  23685. moment.updateOffset(mom, days || months);
  23686. }
  23687. }
  23688. // check if is an array
  23689. function isArray(input) {
  23690. return Object.prototype.toString.call(input) === '[object Array]';
  23691. }
  23692. function isDate(input) {
  23693. return Object.prototype.toString.call(input) === '[object Date]' ||
  23694. input instanceof Date;
  23695. }
  23696. // compare two arrays, return the number of differences
  23697. function compareArrays(array1, array2, dontConvert) {
  23698. var len = Math.min(array1.length, array2.length),
  23699. lengthDiff = Math.abs(array1.length - array2.length),
  23700. diffs = 0,
  23701. i;
  23702. for (i = 0; i < len; i++) {
  23703. if ((dontConvert && array1[i] !== array2[i]) ||
  23704. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  23705. diffs++;
  23706. }
  23707. }
  23708. return diffs + lengthDiff;
  23709. }
  23710. function normalizeUnits(units) {
  23711. if (units) {
  23712. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  23713. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  23714. }
  23715. return units;
  23716. }
  23717. function normalizeObjectUnits(inputObject) {
  23718. var normalizedInput = {},
  23719. normalizedProp,
  23720. prop;
  23721. for (prop in inputObject) {
  23722. if (inputObject.hasOwnProperty(prop)) {
  23723. normalizedProp = normalizeUnits(prop);
  23724. if (normalizedProp) {
  23725. normalizedInput[normalizedProp] = inputObject[prop];
  23726. }
  23727. }
  23728. }
  23729. return normalizedInput;
  23730. }
  23731. function makeList(field) {
  23732. var count, setter;
  23733. if (field.indexOf('week') === 0) {
  23734. count = 7;
  23735. setter = 'day';
  23736. }
  23737. else if (field.indexOf('month') === 0) {
  23738. count = 12;
  23739. setter = 'month';
  23740. }
  23741. else {
  23742. return;
  23743. }
  23744. moment[field] = function (format, index) {
  23745. var i, getter,
  23746. method = moment.fn._lang[field],
  23747. results = [];
  23748. if (typeof format === 'number') {
  23749. index = format;
  23750. format = undefined;
  23751. }
  23752. getter = function (i) {
  23753. var m = moment().utc().set(setter, i);
  23754. return method.call(moment.fn._lang, m, format || '');
  23755. };
  23756. if (index != null) {
  23757. return getter(index);
  23758. }
  23759. else {
  23760. for (i = 0; i < count; i++) {
  23761. results.push(getter(i));
  23762. }
  23763. return results;
  23764. }
  23765. };
  23766. }
  23767. function toInt(argumentForCoercion) {
  23768. var coercedNumber = +argumentForCoercion,
  23769. value = 0;
  23770. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  23771. if (coercedNumber >= 0) {
  23772. value = Math.floor(coercedNumber);
  23773. } else {
  23774. value = Math.ceil(coercedNumber);
  23775. }
  23776. }
  23777. return value;
  23778. }
  23779. function daysInMonth(year, month) {
  23780. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  23781. }
  23782. function weeksInYear(year, dow, doy) {
  23783. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  23784. }
  23785. function daysInYear(year) {
  23786. return isLeapYear(year) ? 366 : 365;
  23787. }
  23788. function isLeapYear(year) {
  23789. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  23790. }
  23791. function checkOverflow(m) {
  23792. var overflow;
  23793. if (m._a && m._pf.overflow === -2) {
  23794. overflow =
  23795. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  23796. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  23797. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  23798. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  23799. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  23800. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  23801. -1;
  23802. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  23803. overflow = DATE;
  23804. }
  23805. m._pf.overflow = overflow;
  23806. }
  23807. }
  23808. function isValid(m) {
  23809. if (m._isValid == null) {
  23810. m._isValid = !isNaN(m._d.getTime()) &&
  23811. m._pf.overflow < 0 &&
  23812. !m._pf.empty &&
  23813. !m._pf.invalidMonth &&
  23814. !m._pf.nullInput &&
  23815. !m._pf.invalidFormat &&
  23816. !m._pf.userInvalidated;
  23817. if (m._strict) {
  23818. m._isValid = m._isValid &&
  23819. m._pf.charsLeftOver === 0 &&
  23820. m._pf.unusedTokens.length === 0;
  23821. }
  23822. }
  23823. return m._isValid;
  23824. }
  23825. function normalizeLanguage(key) {
  23826. return key ? key.toLowerCase().replace('_', '-') : key;
  23827. }
  23828. // Return a moment from input, that is local/utc/zone equivalent to model.
  23829. function makeAs(input, model) {
  23830. return model._isUTC ? moment(input).zone(model._offset || 0) :
  23831. moment(input).local();
  23832. }
  23833. /************************************
  23834. Languages
  23835. ************************************/
  23836. extend(Language.prototype, {
  23837. set : function (config) {
  23838. var prop, i;
  23839. for (i in config) {
  23840. prop = config[i];
  23841. if (typeof prop === 'function') {
  23842. this[i] = prop;
  23843. } else {
  23844. this['_' + i] = prop;
  23845. }
  23846. }
  23847. },
  23848. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  23849. months : function (m) {
  23850. return this._months[m.month()];
  23851. },
  23852. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  23853. monthsShort : function (m) {
  23854. return this._monthsShort[m.month()];
  23855. },
  23856. monthsParse : function (monthName) {
  23857. var i, mom, regex;
  23858. if (!this._monthsParse) {
  23859. this._monthsParse = [];
  23860. }
  23861. for (i = 0; i < 12; i++) {
  23862. // make the regex if we don't have it already
  23863. if (!this._monthsParse[i]) {
  23864. mom = moment.utc([2000, i]);
  23865. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  23866. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  23867. }
  23868. // test the regex
  23869. if (this._monthsParse[i].test(monthName)) {
  23870. return i;
  23871. }
  23872. }
  23873. },
  23874. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  23875. weekdays : function (m) {
  23876. return this._weekdays[m.day()];
  23877. },
  23878. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  23879. weekdaysShort : function (m) {
  23880. return this._weekdaysShort[m.day()];
  23881. },
  23882. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  23883. weekdaysMin : function (m) {
  23884. return this._weekdaysMin[m.day()];
  23885. },
  23886. weekdaysParse : function (weekdayName) {
  23887. var i, mom, regex;
  23888. if (!this._weekdaysParse) {
  23889. this._weekdaysParse = [];
  23890. }
  23891. for (i = 0; i < 7; i++) {
  23892. // make the regex if we don't have it already
  23893. if (!this._weekdaysParse[i]) {
  23894. mom = moment([2000, 1]).day(i);
  23895. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  23896. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  23897. }
  23898. // test the regex
  23899. if (this._weekdaysParse[i].test(weekdayName)) {
  23900. return i;
  23901. }
  23902. }
  23903. },
  23904. _longDateFormat : {
  23905. LT : "h:mm A",
  23906. L : "MM/DD/YYYY",
  23907. LL : "MMMM D YYYY",
  23908. LLL : "MMMM D YYYY LT",
  23909. LLLL : "dddd, MMMM D YYYY LT"
  23910. },
  23911. longDateFormat : function (key) {
  23912. var output = this._longDateFormat[key];
  23913. if (!output && this._longDateFormat[key.toUpperCase()]) {
  23914. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  23915. return val.slice(1);
  23916. });
  23917. this._longDateFormat[key] = output;
  23918. }
  23919. return output;
  23920. },
  23921. isPM : function (input) {
  23922. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  23923. // Using charAt should be more compatible.
  23924. return ((input + '').toLowerCase().charAt(0) === 'p');
  23925. },
  23926. _meridiemParse : /[ap]\.?m?\.?/i,
  23927. meridiem : function (hours, minutes, isLower) {
  23928. if (hours > 11) {
  23929. return isLower ? 'pm' : 'PM';
  23930. } else {
  23931. return isLower ? 'am' : 'AM';
  23932. }
  23933. },
  23934. _calendar : {
  23935. sameDay : '[Today at] LT',
  23936. nextDay : '[Tomorrow at] LT',
  23937. nextWeek : 'dddd [at] LT',
  23938. lastDay : '[Yesterday at] LT',
  23939. lastWeek : '[Last] dddd [at] LT',
  23940. sameElse : 'L'
  23941. },
  23942. calendar : function (key, mom) {
  23943. var output = this._calendar[key];
  23944. return typeof output === 'function' ? output.apply(mom) : output;
  23945. },
  23946. _relativeTime : {
  23947. future : "in %s",
  23948. past : "%s ago",
  23949. s : "a few seconds",
  23950. m : "a minute",
  23951. mm : "%d minutes",
  23952. h : "an hour",
  23953. hh : "%d hours",
  23954. d : "a day",
  23955. dd : "%d days",
  23956. M : "a month",
  23957. MM : "%d months",
  23958. y : "a year",
  23959. yy : "%d years"
  23960. },
  23961. relativeTime : function (number, withoutSuffix, string, isFuture) {
  23962. var output = this._relativeTime[string];
  23963. return (typeof output === 'function') ?
  23964. output(number, withoutSuffix, string, isFuture) :
  23965. output.replace(/%d/i, number);
  23966. },
  23967. pastFuture : function (diff, output) {
  23968. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  23969. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  23970. },
  23971. ordinal : function (number) {
  23972. return this._ordinal.replace("%d", number);
  23973. },
  23974. _ordinal : "%d",
  23975. preparse : function (string) {
  23976. return string;
  23977. },
  23978. postformat : function (string) {
  23979. return string;
  23980. },
  23981. week : function (mom) {
  23982. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  23983. },
  23984. _week : {
  23985. dow : 0, // Sunday is the first day of the week.
  23986. doy : 6 // The week that contains Jan 1st is the first week of the year.
  23987. },
  23988. _invalidDate: 'Invalid date',
  23989. invalidDate: function () {
  23990. return this._invalidDate;
  23991. }
  23992. });
  23993. // Loads a language definition into the `languages` cache. The function
  23994. // takes a key and optionally values. If not in the browser and no values
  23995. // are provided, it will load the language file module. As a convenience,
  23996. // this function also returns the language values.
  23997. function loadLang(key, values) {
  23998. values.abbr = key;
  23999. if (!languages[key]) {
  24000. languages[key] = new Language();
  24001. }
  24002. languages[key].set(values);
  24003. return languages[key];
  24004. }
  24005. // Remove a language from the `languages` cache. Mostly useful in tests.
  24006. function unloadLang(key) {
  24007. delete languages[key];
  24008. }
  24009. // Determines which language definition to use and returns it.
  24010. //
  24011. // With no parameters, it will return the global language. If you
  24012. // pass in a language key, such as 'en', it will return the
  24013. // definition for 'en', so long as 'en' has already been loaded using
  24014. // moment.lang.
  24015. function getLangDefinition(key) {
  24016. var i = 0, j, lang, next, split,
  24017. get = function (k) {
  24018. if (!languages[k] && hasModule) {
  24019. try {
  24020. require('./lang/' + k);
  24021. } catch (e) { }
  24022. }
  24023. return languages[k];
  24024. };
  24025. if (!key) {
  24026. return moment.fn._lang;
  24027. }
  24028. if (!isArray(key)) {
  24029. //short-circuit everything else
  24030. lang = get(key);
  24031. if (lang) {
  24032. return lang;
  24033. }
  24034. key = [key];
  24035. }
  24036. //pick the language from the array
  24037. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  24038. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  24039. while (i < key.length) {
  24040. split = normalizeLanguage(key[i]).split('-');
  24041. j = split.length;
  24042. next = normalizeLanguage(key[i + 1]);
  24043. next = next ? next.split('-') : null;
  24044. while (j > 0) {
  24045. lang = get(split.slice(0, j).join('-'));
  24046. if (lang) {
  24047. return lang;
  24048. }
  24049. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  24050. //the next array item is better than a shallower substring of this one
  24051. break;
  24052. }
  24053. j--;
  24054. }
  24055. i++;
  24056. }
  24057. return moment.fn._lang;
  24058. }
  24059. /************************************
  24060. Formatting
  24061. ************************************/
  24062. function removeFormattingTokens(input) {
  24063. if (input.match(/\[[\s\S]/)) {
  24064. return input.replace(/^\[|\]$/g, "");
  24065. }
  24066. return input.replace(/\\/g, "");
  24067. }
  24068. function makeFormatFunction(format) {
  24069. var array = format.match(formattingTokens), i, length;
  24070. for (i = 0, length = array.length; i < length; i++) {
  24071. if (formatTokenFunctions[array[i]]) {
  24072. array[i] = formatTokenFunctions[array[i]];
  24073. } else {
  24074. array[i] = removeFormattingTokens(array[i]);
  24075. }
  24076. }
  24077. return function (mom) {
  24078. var output = "";
  24079. for (i = 0; i < length; i++) {
  24080. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  24081. }
  24082. return output;
  24083. };
  24084. }
  24085. // format date using native date object
  24086. function formatMoment(m, format) {
  24087. if (!m.isValid()) {
  24088. return m.lang().invalidDate();
  24089. }
  24090. format = expandFormat(format, m.lang());
  24091. if (!formatFunctions[format]) {
  24092. formatFunctions[format] = makeFormatFunction(format);
  24093. }
  24094. return formatFunctions[format](m);
  24095. }
  24096. function expandFormat(format, lang) {
  24097. var i = 5;
  24098. function replaceLongDateFormatTokens(input) {
  24099. return lang.longDateFormat(input) || input;
  24100. }
  24101. localFormattingTokens.lastIndex = 0;
  24102. while (i >= 0 && localFormattingTokens.test(format)) {
  24103. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  24104. localFormattingTokens.lastIndex = 0;
  24105. i -= 1;
  24106. }
  24107. return format;
  24108. }
  24109. /************************************
  24110. Parsing
  24111. ************************************/
  24112. // get the regex to find the next token
  24113. function getParseRegexForToken(token, config) {
  24114. var a, strict = config._strict;
  24115. switch (token) {
  24116. case 'Q':
  24117. return parseTokenOneDigit;
  24118. case 'DDDD':
  24119. return parseTokenThreeDigits;
  24120. case 'YYYY':
  24121. case 'GGGG':
  24122. case 'gggg':
  24123. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  24124. case 'Y':
  24125. case 'G':
  24126. case 'g':
  24127. return parseTokenSignedNumber;
  24128. case 'YYYYYY':
  24129. case 'YYYYY':
  24130. case 'GGGGG':
  24131. case 'ggggg':
  24132. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  24133. case 'S':
  24134. if (strict) { return parseTokenOneDigit; }
  24135. /* falls through */
  24136. case 'SS':
  24137. if (strict) { return parseTokenTwoDigits; }
  24138. /* falls through */
  24139. case 'SSS':
  24140. if (strict) { return parseTokenThreeDigits; }
  24141. /* falls through */
  24142. case 'DDD':
  24143. return parseTokenOneToThreeDigits;
  24144. case 'MMM':
  24145. case 'MMMM':
  24146. case 'dd':
  24147. case 'ddd':
  24148. case 'dddd':
  24149. return parseTokenWord;
  24150. case 'a':
  24151. case 'A':
  24152. return getLangDefinition(config._l)._meridiemParse;
  24153. case 'X':
  24154. return parseTokenTimestampMs;
  24155. case 'Z':
  24156. case 'ZZ':
  24157. return parseTokenTimezone;
  24158. case 'T':
  24159. return parseTokenT;
  24160. case 'SSSS':
  24161. return parseTokenDigits;
  24162. case 'MM':
  24163. case 'DD':
  24164. case 'YY':
  24165. case 'GG':
  24166. case 'gg':
  24167. case 'HH':
  24168. case 'hh':
  24169. case 'mm':
  24170. case 'ss':
  24171. case 'ww':
  24172. case 'WW':
  24173. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  24174. case 'M':
  24175. case 'D':
  24176. case 'd':
  24177. case 'H':
  24178. case 'h':
  24179. case 'm':
  24180. case 's':
  24181. case 'w':
  24182. case 'W':
  24183. case 'e':
  24184. case 'E':
  24185. return parseTokenOneOrTwoDigits;
  24186. case 'Do':
  24187. return parseTokenOrdinal;
  24188. default :
  24189. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  24190. return a;
  24191. }
  24192. }
  24193. function timezoneMinutesFromString(string) {
  24194. string = string || "";
  24195. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  24196. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  24197. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  24198. minutes = +(parts[1] * 60) + toInt(parts[2]);
  24199. return parts[0] === '+' ? -minutes : minutes;
  24200. }
  24201. // function to convert string input to date
  24202. function addTimeToArrayFromToken(token, input, config) {
  24203. var a, datePartArray = config._a;
  24204. switch (token) {
  24205. // QUARTER
  24206. case 'Q':
  24207. if (input != null) {
  24208. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  24209. }
  24210. break;
  24211. // MONTH
  24212. case 'M' : // fall through to MM
  24213. case 'MM' :
  24214. if (input != null) {
  24215. datePartArray[MONTH] = toInt(input) - 1;
  24216. }
  24217. break;
  24218. case 'MMM' : // fall through to MMMM
  24219. case 'MMMM' :
  24220. a = getLangDefinition(config._l).monthsParse(input);
  24221. // if we didn't find a month name, mark the date as invalid.
  24222. if (a != null) {
  24223. datePartArray[MONTH] = a;
  24224. } else {
  24225. config._pf.invalidMonth = input;
  24226. }
  24227. break;
  24228. // DAY OF MONTH
  24229. case 'D' : // fall through to DD
  24230. case 'DD' :
  24231. if (input != null) {
  24232. datePartArray[DATE] = toInt(input);
  24233. }
  24234. break;
  24235. case 'Do' :
  24236. if (input != null) {
  24237. datePartArray[DATE] = toInt(parseInt(input, 10));
  24238. }
  24239. break;
  24240. // DAY OF YEAR
  24241. case 'DDD' : // fall through to DDDD
  24242. case 'DDDD' :
  24243. if (input != null) {
  24244. config._dayOfYear = toInt(input);
  24245. }
  24246. break;
  24247. // YEAR
  24248. case 'YY' :
  24249. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  24250. break;
  24251. case 'YYYY' :
  24252. case 'YYYYY' :
  24253. case 'YYYYYY' :
  24254. datePartArray[YEAR] = toInt(input);
  24255. break;
  24256. // AM / PM
  24257. case 'a' : // fall through to A
  24258. case 'A' :
  24259. config._isPm = getLangDefinition(config._l).isPM(input);
  24260. break;
  24261. // 24 HOUR
  24262. case 'H' : // fall through to hh
  24263. case 'HH' : // fall through to hh
  24264. case 'h' : // fall through to hh
  24265. case 'hh' :
  24266. datePartArray[HOUR] = toInt(input);
  24267. break;
  24268. // MINUTE
  24269. case 'm' : // fall through to mm
  24270. case 'mm' :
  24271. datePartArray[MINUTE] = toInt(input);
  24272. break;
  24273. // SECOND
  24274. case 's' : // fall through to ss
  24275. case 'ss' :
  24276. datePartArray[SECOND] = toInt(input);
  24277. break;
  24278. // MILLISECOND
  24279. case 'S' :
  24280. case 'SS' :
  24281. case 'SSS' :
  24282. case 'SSSS' :
  24283. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  24284. break;
  24285. // UNIX TIMESTAMP WITH MS
  24286. case 'X':
  24287. config._d = new Date(parseFloat(input) * 1000);
  24288. break;
  24289. // TIMEZONE
  24290. case 'Z' : // fall through to ZZ
  24291. case 'ZZ' :
  24292. config._useUTC = true;
  24293. config._tzm = timezoneMinutesFromString(input);
  24294. break;
  24295. // WEEKDAY - human
  24296. case 'dd':
  24297. case 'ddd':
  24298. case 'dddd':
  24299. a = getLangDefinition(config._l).weekdaysParse(input);
  24300. // if we didn't get a weekday name, mark the date as invalid
  24301. if (a != null) {
  24302. config._w = config._w || {};
  24303. config._w['d'] = a;
  24304. } else {
  24305. config._pf.invalidWeekday = input;
  24306. }
  24307. break;
  24308. // WEEK, WEEK DAY - numeric
  24309. case 'w':
  24310. case 'ww':
  24311. case 'W':
  24312. case 'WW':
  24313. case 'd':
  24314. case 'e':
  24315. case 'E':
  24316. token = token.substr(0, 1);
  24317. /* falls through */
  24318. case 'gggg':
  24319. case 'GGGG':
  24320. case 'GGGGG':
  24321. token = token.substr(0, 2);
  24322. if (input) {
  24323. config._w = config._w || {};
  24324. config._w[token] = toInt(input);
  24325. }
  24326. break;
  24327. case 'gg':
  24328. case 'GG':
  24329. config._w = config._w || {};
  24330. config._w[token] = moment.parseTwoDigitYear(input);
  24331. }
  24332. }
  24333. function dayOfYearFromWeekInfo(config) {
  24334. var w, weekYear, week, weekday, dow, doy, temp, lang;
  24335. w = config._w;
  24336. if (w.GG != null || w.W != null || w.E != null) {
  24337. dow = 1;
  24338. doy = 4;
  24339. // TODO: We need to take the current isoWeekYear, but that depends on
  24340. // how we interpret now (local, utc, fixed offset). So create
  24341. // a now version of current config (take local/utc/offset flags, and
  24342. // create now).
  24343. weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
  24344. week = dfl(w.W, 1);
  24345. weekday = dfl(w.E, 1);
  24346. } else {
  24347. lang = getLangDefinition(config._l);
  24348. dow = lang._week.dow;
  24349. doy = lang._week.doy;
  24350. weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
  24351. week = dfl(w.w, 1);
  24352. if (w.d != null) {
  24353. // weekday -- low day numbers are considered next week
  24354. weekday = w.d;
  24355. if (weekday < dow) {
  24356. ++week;
  24357. }
  24358. } else if (w.e != null) {
  24359. // local weekday -- counting starts from begining of week
  24360. weekday = w.e + dow;
  24361. } else {
  24362. // default to begining of week
  24363. weekday = dow;
  24364. }
  24365. }
  24366. temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
  24367. config._a[YEAR] = temp.year;
  24368. config._dayOfYear = temp.dayOfYear;
  24369. }
  24370. // convert an array to a date.
  24371. // the array should mirror the parameters below
  24372. // note: all values past the year are optional and will default to the lowest possible value.
  24373. // [year, month, day , hour, minute, second, millisecond]
  24374. function dateFromConfig(config) {
  24375. var i, date, input = [], currentDate, yearToUse;
  24376. if (config._d) {
  24377. return;
  24378. }
  24379. currentDate = currentDateArray(config);
  24380. //compute day of the year from weeks and weekdays
  24381. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  24382. dayOfYearFromWeekInfo(config);
  24383. }
  24384. //if the day of the year is set, figure out what it is
  24385. if (config._dayOfYear) {
  24386. yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
  24387. if (config._dayOfYear > daysInYear(yearToUse)) {
  24388. config._pf._overflowDayOfYear = true;
  24389. }
  24390. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  24391. config._a[MONTH] = date.getUTCMonth();
  24392. config._a[DATE] = date.getUTCDate();
  24393. }
  24394. // Default to current date.
  24395. // * if no year, month, day of month are given, default to today
  24396. // * if day of month is given, default month and year
  24397. // * if month is given, default only year
  24398. // * if year is given, don't default anything
  24399. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  24400. config._a[i] = input[i] = currentDate[i];
  24401. }
  24402. // Zero out whatever was not defaulted, including time
  24403. for (; i < 7; i++) {
  24404. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  24405. }
  24406. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  24407. // Apply timezone offset from input. The actual zone can be changed
  24408. // with parseZone.
  24409. if (config._tzm != null) {
  24410. config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
  24411. }
  24412. }
  24413. function dateFromObject(config) {
  24414. var normalizedInput;
  24415. if (config._d) {
  24416. return;
  24417. }
  24418. normalizedInput = normalizeObjectUnits(config._i);
  24419. config._a = [
  24420. normalizedInput.year,
  24421. normalizedInput.month,
  24422. normalizedInput.day,
  24423. normalizedInput.hour,
  24424. normalizedInput.minute,
  24425. normalizedInput.second,
  24426. normalizedInput.millisecond
  24427. ];
  24428. dateFromConfig(config);
  24429. }
  24430. function currentDateArray(config) {
  24431. var now = new Date();
  24432. if (config._useUTC) {
  24433. return [
  24434. now.getUTCFullYear(),
  24435. now.getUTCMonth(),
  24436. now.getUTCDate()
  24437. ];
  24438. } else {
  24439. return [now.getFullYear(), now.getMonth(), now.getDate()];
  24440. }
  24441. }
  24442. // date from string and format string
  24443. function makeDateFromStringAndFormat(config) {
  24444. if (config._f === moment.ISO_8601) {
  24445. parseISO(config);
  24446. return;
  24447. }
  24448. config._a = [];
  24449. config._pf.empty = true;
  24450. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  24451. var lang = getLangDefinition(config._l),
  24452. string = '' + config._i,
  24453. i, parsedInput, tokens, token, skipped,
  24454. stringLength = string.length,
  24455. totalParsedInputLength = 0;
  24456. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  24457. for (i = 0; i < tokens.length; i++) {
  24458. token = tokens[i];
  24459. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  24460. if (parsedInput) {
  24461. skipped = string.substr(0, string.indexOf(parsedInput));
  24462. if (skipped.length > 0) {
  24463. config._pf.unusedInput.push(skipped);
  24464. }
  24465. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  24466. totalParsedInputLength += parsedInput.length;
  24467. }
  24468. // don't parse if it's not a known token
  24469. if (formatTokenFunctions[token]) {
  24470. if (parsedInput) {
  24471. config._pf.empty = false;
  24472. }
  24473. else {
  24474. config._pf.unusedTokens.push(token);
  24475. }
  24476. addTimeToArrayFromToken(token, parsedInput, config);
  24477. }
  24478. else if (config._strict && !parsedInput) {
  24479. config._pf.unusedTokens.push(token);
  24480. }
  24481. }
  24482. // add remaining unparsed input length to the string
  24483. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  24484. if (string.length > 0) {
  24485. config._pf.unusedInput.push(string);
  24486. }
  24487. // handle am pm
  24488. if (config._isPm && config._a[HOUR] < 12) {
  24489. config._a[HOUR] += 12;
  24490. }
  24491. // if is 12 am, change hours to 0
  24492. if (config._isPm === false && config._a[HOUR] === 12) {
  24493. config._a[HOUR] = 0;
  24494. }
  24495. dateFromConfig(config);
  24496. checkOverflow(config);
  24497. }
  24498. function unescapeFormat(s) {
  24499. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  24500. return p1 || p2 || p3 || p4;
  24501. });
  24502. }
  24503. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  24504. function regexpEscape(s) {
  24505. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  24506. }
  24507. // date from string and array of format strings
  24508. function makeDateFromStringAndArray(config) {
  24509. var tempConfig,
  24510. bestMoment,
  24511. scoreToBeat,
  24512. i,
  24513. currentScore;
  24514. if (config._f.length === 0) {
  24515. config._pf.invalidFormat = true;
  24516. config._d = new Date(NaN);
  24517. return;
  24518. }
  24519. for (i = 0; i < config._f.length; i++) {
  24520. currentScore = 0;
  24521. tempConfig = extend({}, config);
  24522. tempConfig._pf = defaultParsingFlags();
  24523. tempConfig._f = config._f[i];
  24524. makeDateFromStringAndFormat(tempConfig);
  24525. if (!isValid(tempConfig)) {
  24526. continue;
  24527. }
  24528. // if there is any input that was not parsed add a penalty for that format
  24529. currentScore += tempConfig._pf.charsLeftOver;
  24530. //or tokens
  24531. currentScore += tempConfig._pf.unusedTokens.length * 10;
  24532. tempConfig._pf.score = currentScore;
  24533. if (scoreToBeat == null || currentScore < scoreToBeat) {
  24534. scoreToBeat = currentScore;
  24535. bestMoment = tempConfig;
  24536. }
  24537. }
  24538. extend(config, bestMoment || tempConfig);
  24539. }
  24540. // date from iso format
  24541. function parseISO(config) {
  24542. var i, l,
  24543. string = config._i,
  24544. match = isoRegex.exec(string);
  24545. if (match) {
  24546. config._pf.iso = true;
  24547. for (i = 0, l = isoDates.length; i < l; i++) {
  24548. if (isoDates[i][1].exec(string)) {
  24549. // match[5] should be "T" or undefined
  24550. config._f = isoDates[i][0] + (match[6] || " ");
  24551. break;
  24552. }
  24553. }
  24554. for (i = 0, l = isoTimes.length; i < l; i++) {
  24555. if (isoTimes[i][1].exec(string)) {
  24556. config._f += isoTimes[i][0];
  24557. break;
  24558. }
  24559. }
  24560. if (string.match(parseTokenTimezone)) {
  24561. config._f += "Z";
  24562. }
  24563. makeDateFromStringAndFormat(config);
  24564. } else {
  24565. config._isValid = false;
  24566. }
  24567. }
  24568. // date from iso format or fallback
  24569. function makeDateFromString(config) {
  24570. parseISO(config);
  24571. if (config._isValid === false) {
  24572. delete config._isValid;
  24573. moment.createFromInputFallback(config);
  24574. }
  24575. }
  24576. function makeDateFromInput(config) {
  24577. var input = config._i,
  24578. matched = aspNetJsonRegex.exec(input);
  24579. if (input === undefined) {
  24580. config._d = new Date();
  24581. } else if (matched) {
  24582. config._d = new Date(+matched[1]);
  24583. } else if (typeof input === 'string') {
  24584. makeDateFromString(config);
  24585. } else if (isArray(input)) {
  24586. config._a = input.slice(0);
  24587. dateFromConfig(config);
  24588. } else if (isDate(input)) {
  24589. config._d = new Date(+input);
  24590. } else if (typeof(input) === 'object') {
  24591. dateFromObject(config);
  24592. } else if (typeof(input) === 'number') {
  24593. // from milliseconds
  24594. config._d = new Date(input);
  24595. } else {
  24596. moment.createFromInputFallback(config);
  24597. }
  24598. }
  24599. function makeDate(y, m, d, h, M, s, ms) {
  24600. //can't just apply() to create a date:
  24601. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  24602. var date = new Date(y, m, d, h, M, s, ms);
  24603. //the date constructor doesn't accept years < 1970
  24604. if (y < 1970) {
  24605. date.setFullYear(y);
  24606. }
  24607. return date;
  24608. }
  24609. function makeUTCDate(y) {
  24610. var date = new Date(Date.UTC.apply(null, arguments));
  24611. if (y < 1970) {
  24612. date.setUTCFullYear(y);
  24613. }
  24614. return date;
  24615. }
  24616. function parseWeekday(input, language) {
  24617. if (typeof input === 'string') {
  24618. if (!isNaN(input)) {
  24619. input = parseInt(input, 10);
  24620. }
  24621. else {
  24622. input = language.weekdaysParse(input);
  24623. if (typeof input !== 'number') {
  24624. return null;
  24625. }
  24626. }
  24627. }
  24628. return input;
  24629. }
  24630. /************************************
  24631. Relative Time
  24632. ************************************/
  24633. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  24634. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  24635. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  24636. }
  24637. function relativeTime(milliseconds, withoutSuffix, lang) {
  24638. var seconds = round(Math.abs(milliseconds) / 1000),
  24639. minutes = round(seconds / 60),
  24640. hours = round(minutes / 60),
  24641. days = round(hours / 24),
  24642. years = round(days / 365),
  24643. args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
  24644. minutes === 1 && ['m'] ||
  24645. minutes < relativeTimeThresholds.m && ['mm', minutes] ||
  24646. hours === 1 && ['h'] ||
  24647. hours < relativeTimeThresholds.h && ['hh', hours] ||
  24648. days === 1 && ['d'] ||
  24649. days <= relativeTimeThresholds.dd && ['dd', days] ||
  24650. days <= relativeTimeThresholds.dm && ['M'] ||
  24651. days < relativeTimeThresholds.dy && ['MM', round(days / 30)] ||
  24652. years === 1 && ['y'] || ['yy', years];
  24653. args[2] = withoutSuffix;
  24654. args[3] = milliseconds > 0;
  24655. args[4] = lang;
  24656. return substituteTimeAgo.apply({}, args);
  24657. }
  24658. /************************************
  24659. Week of Year
  24660. ************************************/
  24661. // firstDayOfWeek 0 = sun, 6 = sat
  24662. // the day of the week that starts the week
  24663. // (usually sunday or monday)
  24664. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  24665. // the first week is the week that contains the first
  24666. // of this day of the week
  24667. // (eg. ISO weeks use thursday (4))
  24668. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  24669. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  24670. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  24671. adjustedMoment;
  24672. if (daysToDayOfWeek > end) {
  24673. daysToDayOfWeek -= 7;
  24674. }
  24675. if (daysToDayOfWeek < end - 7) {
  24676. daysToDayOfWeek += 7;
  24677. }
  24678. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  24679. return {
  24680. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  24681. year: adjustedMoment.year()
  24682. };
  24683. }
  24684. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  24685. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  24686. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  24687. d = d === 0 ? 7 : d;
  24688. weekday = weekday != null ? weekday : firstDayOfWeek;
  24689. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  24690. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  24691. return {
  24692. year: dayOfYear > 0 ? year : year - 1,
  24693. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  24694. };
  24695. }
  24696. /************************************
  24697. Top Level Functions
  24698. ************************************/
  24699. function makeMoment(config) {
  24700. var input = config._i,
  24701. format = config._f;
  24702. if (input === null || (format === undefined && input === '')) {
  24703. return moment.invalid({nullInput: true});
  24704. }
  24705. if (typeof input === 'string') {
  24706. config._i = input = getLangDefinition().preparse(input);
  24707. }
  24708. if (moment.isMoment(input)) {
  24709. config = cloneMoment(input);
  24710. config._d = new Date(+input._d);
  24711. } else if (format) {
  24712. if (isArray(format)) {
  24713. makeDateFromStringAndArray(config);
  24714. } else {
  24715. makeDateFromStringAndFormat(config);
  24716. }
  24717. } else {
  24718. makeDateFromInput(config);
  24719. }
  24720. return new Moment(config);
  24721. }
  24722. moment = function (input, format, lang, strict) {
  24723. var c;
  24724. if (typeof(lang) === "boolean") {
  24725. strict = lang;
  24726. lang = undefined;
  24727. }
  24728. // object construction must be done this way.
  24729. // https://github.com/moment/moment/issues/1423
  24730. c = {};
  24731. c._isAMomentObject = true;
  24732. c._i = input;
  24733. c._f = format;
  24734. c._l = lang;
  24735. c._strict = strict;
  24736. c._isUTC = false;
  24737. c._pf = defaultParsingFlags();
  24738. return makeMoment(c);
  24739. };
  24740. moment.suppressDeprecationWarnings = false;
  24741. moment.createFromInputFallback = deprecate(
  24742. "moment construction falls back to js Date. This is " +
  24743. "discouraged and will be removed in upcoming major " +
  24744. "release. Please refer to " +
  24745. "https://github.com/moment/moment/issues/1407 for more info.",
  24746. function (config) {
  24747. config._d = new Date(config._i);
  24748. });
  24749. // Pick a moment m from moments so that m[fn](other) is true for all
  24750. // other. This relies on the function fn to be transitive.
  24751. //
  24752. // moments should either be an array of moment objects or an array, whose
  24753. // first element is an array of moment objects.
  24754. function pickBy(fn, moments) {
  24755. var res, i;
  24756. if (moments.length === 1 && isArray(moments[0])) {
  24757. moments = moments[0];
  24758. }
  24759. if (!moments.length) {
  24760. return moment();
  24761. }
  24762. res = moments[0];
  24763. for (i = 1; i < moments.length; ++i) {
  24764. if (moments[i][fn](res)) {
  24765. res = moments[i];
  24766. }
  24767. }
  24768. return res;
  24769. }
  24770. moment.min = function () {
  24771. var args = [].slice.call(arguments, 0);
  24772. return pickBy('isBefore', args);
  24773. };
  24774. moment.max = function () {
  24775. var args = [].slice.call(arguments, 0);
  24776. return pickBy('isAfter', args);
  24777. };
  24778. // creating with utc
  24779. moment.utc = function (input, format, lang, strict) {
  24780. var c;
  24781. if (typeof(lang) === "boolean") {
  24782. strict = lang;
  24783. lang = undefined;
  24784. }
  24785. // object construction must be done this way.
  24786. // https://github.com/moment/moment/issues/1423
  24787. c = {};
  24788. c._isAMomentObject = true;
  24789. c._useUTC = true;
  24790. c._isUTC = true;
  24791. c._l = lang;
  24792. c._i = input;
  24793. c._f = format;
  24794. c._strict = strict;
  24795. c._pf = defaultParsingFlags();
  24796. return makeMoment(c).utc();
  24797. };
  24798. // creating with unix timestamp (in seconds)
  24799. moment.unix = function (input) {
  24800. return moment(input * 1000);
  24801. };
  24802. // duration
  24803. moment.duration = function (input, key) {
  24804. var duration = input,
  24805. // matching against regexp is expensive, do it on demand
  24806. match = null,
  24807. sign,
  24808. ret,
  24809. parseIso;
  24810. if (moment.isDuration(input)) {
  24811. duration = {
  24812. ms: input._milliseconds,
  24813. d: input._days,
  24814. M: input._months
  24815. };
  24816. } else if (typeof input === 'number') {
  24817. duration = {};
  24818. if (key) {
  24819. duration[key] = input;
  24820. } else {
  24821. duration.milliseconds = input;
  24822. }
  24823. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  24824. sign = (match[1] === "-") ? -1 : 1;
  24825. duration = {
  24826. y: 0,
  24827. d: toInt(match[DATE]) * sign,
  24828. h: toInt(match[HOUR]) * sign,
  24829. m: toInt(match[MINUTE]) * sign,
  24830. s: toInt(match[SECOND]) * sign,
  24831. ms: toInt(match[MILLISECOND]) * sign
  24832. };
  24833. } else if (!!(match = isoDurationRegex.exec(input))) {
  24834. sign = (match[1] === "-") ? -1 : 1;
  24835. parseIso = function (inp) {
  24836. // We'd normally use ~~inp for this, but unfortunately it also
  24837. // converts floats to ints.
  24838. // inp may be undefined, so careful calling replace on it.
  24839. var res = inp && parseFloat(inp.replace(',', '.'));
  24840. // apply sign while we're at it
  24841. return (isNaN(res) ? 0 : res) * sign;
  24842. };
  24843. duration = {
  24844. y: parseIso(match[2]),
  24845. M: parseIso(match[3]),
  24846. d: parseIso(match[4]),
  24847. h: parseIso(match[5]),
  24848. m: parseIso(match[6]),
  24849. s: parseIso(match[7]),
  24850. w: parseIso(match[8])
  24851. };
  24852. }
  24853. ret = new Duration(duration);
  24854. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  24855. ret._lang = input._lang;
  24856. }
  24857. return ret;
  24858. };
  24859. // version number
  24860. moment.version = VERSION;
  24861. // default format
  24862. moment.defaultFormat = isoFormat;
  24863. // constant that refers to the ISO standard
  24864. moment.ISO_8601 = function () {};
  24865. // Plugins that add properties should also add the key here (null value),
  24866. // so we can properly clone ourselves.
  24867. moment.momentProperties = momentProperties;
  24868. // This function will be called whenever a moment is mutated.
  24869. // It is intended to keep the offset in sync with the timezone.
  24870. moment.updateOffset = function () {};
  24871. // This function allows you to set a threshold for relative time strings
  24872. moment.relativeTimeThreshold = function(threshold, limit) {
  24873. if (relativeTimeThresholds[threshold] === undefined) {
  24874. return false;
  24875. }
  24876. relativeTimeThresholds[threshold] = limit;
  24877. return true;
  24878. };
  24879. // This function will load languages and then set the global language. If
  24880. // no arguments are passed in, it will simply return the current global
  24881. // language key.
  24882. moment.lang = function (key, values) {
  24883. var r;
  24884. if (!key) {
  24885. return moment.fn._lang._abbr;
  24886. }
  24887. if (values) {
  24888. loadLang(normalizeLanguage(key), values);
  24889. } else if (values === null) {
  24890. unloadLang(key);
  24891. key = 'en';
  24892. } else if (!languages[key]) {
  24893. getLangDefinition(key);
  24894. }
  24895. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  24896. return r._abbr;
  24897. };
  24898. // returns language data
  24899. moment.langData = function (key) {
  24900. if (key && key._lang && key._lang._abbr) {
  24901. key = key._lang._abbr;
  24902. }
  24903. return getLangDefinition(key);
  24904. };
  24905. // compare moment object
  24906. moment.isMoment = function (obj) {
  24907. return obj instanceof Moment ||
  24908. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  24909. };
  24910. // for typechecking Duration objects
  24911. moment.isDuration = function (obj) {
  24912. return obj instanceof Duration;
  24913. };
  24914. for (i = lists.length - 1; i >= 0; --i) {
  24915. makeList(lists[i]);
  24916. }
  24917. moment.normalizeUnits = function (units) {
  24918. return normalizeUnits(units);
  24919. };
  24920. moment.invalid = function (flags) {
  24921. var m = moment.utc(NaN);
  24922. if (flags != null) {
  24923. extend(m._pf, flags);
  24924. }
  24925. else {
  24926. m._pf.userInvalidated = true;
  24927. }
  24928. return m;
  24929. };
  24930. moment.parseZone = function () {
  24931. return moment.apply(null, arguments).parseZone();
  24932. };
  24933. moment.parseTwoDigitYear = function (input) {
  24934. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  24935. };
  24936. /************************************
  24937. Moment Prototype
  24938. ************************************/
  24939. extend(moment.fn = Moment.prototype, {
  24940. clone : function () {
  24941. return moment(this);
  24942. },
  24943. valueOf : function () {
  24944. return +this._d + ((this._offset || 0) * 60000);
  24945. },
  24946. unix : function () {
  24947. return Math.floor(+this / 1000);
  24948. },
  24949. toString : function () {
  24950. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  24951. },
  24952. toDate : function () {
  24953. return this._offset ? new Date(+this) : this._d;
  24954. },
  24955. toISOString : function () {
  24956. var m = moment(this).utc();
  24957. if (0 < m.year() && m.year() <= 9999) {
  24958. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  24959. } else {
  24960. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  24961. }
  24962. },
  24963. toArray : function () {
  24964. var m = this;
  24965. return [
  24966. m.year(),
  24967. m.month(),
  24968. m.date(),
  24969. m.hours(),
  24970. m.minutes(),
  24971. m.seconds(),
  24972. m.milliseconds()
  24973. ];
  24974. },
  24975. isValid : function () {
  24976. return isValid(this);
  24977. },
  24978. isDSTShifted : function () {
  24979. if (this._a) {
  24980. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  24981. }
  24982. return false;
  24983. },
  24984. parsingFlags : function () {
  24985. return extend({}, this._pf);
  24986. },
  24987. invalidAt: function () {
  24988. return this._pf.overflow;
  24989. },
  24990. utc : function () {
  24991. return this.zone(0);
  24992. },
  24993. local : function () {
  24994. this.zone(0);
  24995. this._isUTC = false;
  24996. return this;
  24997. },
  24998. format : function (inputString) {
  24999. var output = formatMoment(this, inputString || moment.defaultFormat);
  25000. return this.lang().postformat(output);
  25001. },
  25002. add : function (input, val) {
  25003. var dur;
  25004. // switch args to support add('s', 1) and add(1, 's')
  25005. if (typeof input === 'string' && typeof val === 'string') {
  25006. dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
  25007. } else if (typeof input === 'string') {
  25008. dur = moment.duration(+val, input);
  25009. } else {
  25010. dur = moment.duration(input, val);
  25011. }
  25012. addOrSubtractDurationFromMoment(this, dur, 1);
  25013. return this;
  25014. },
  25015. subtract : function (input, val) {
  25016. var dur;
  25017. // switch args to support subtract('s', 1) and subtract(1, 's')
  25018. if (typeof input === 'string' && typeof val === 'string') {
  25019. dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
  25020. } else if (typeof input === 'string') {
  25021. dur = moment.duration(+val, input);
  25022. } else {
  25023. dur = moment.duration(input, val);
  25024. }
  25025. addOrSubtractDurationFromMoment(this, dur, -1);
  25026. return this;
  25027. },
  25028. diff : function (input, units, asFloat) {
  25029. var that = makeAs(input, this),
  25030. zoneDiff = (this.zone() - that.zone()) * 6e4,
  25031. diff, output;
  25032. units = normalizeUnits(units);
  25033. if (units === 'year' || units === 'month') {
  25034. // average number of days in the months in the given dates
  25035. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  25036. // difference in months
  25037. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  25038. // adjust by taking difference in days, average number of days
  25039. // and dst in the given months.
  25040. output += ((this - moment(this).startOf('month')) -
  25041. (that - moment(that).startOf('month'))) / diff;
  25042. // same as above but with zones, to negate all dst
  25043. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  25044. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  25045. if (units === 'year') {
  25046. output = output / 12;
  25047. }
  25048. } else {
  25049. diff = (this - that);
  25050. output = units === 'second' ? diff / 1e3 : // 1000
  25051. units === 'minute' ? diff / 6e4 : // 1000 * 60
  25052. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  25053. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  25054. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  25055. diff;
  25056. }
  25057. return asFloat ? output : absRound(output);
  25058. },
  25059. from : function (time, withoutSuffix) {
  25060. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  25061. },
  25062. fromNow : function (withoutSuffix) {
  25063. return this.from(moment(), withoutSuffix);
  25064. },
  25065. calendar : function (time) {
  25066. // We want to compare the start of today, vs this.
  25067. // Getting start-of-today depends on whether we're zone'd or not.
  25068. var now = time || moment(),
  25069. sod = makeAs(now, this).startOf('day'),
  25070. diff = this.diff(sod, 'days', true),
  25071. format = diff < -6 ? 'sameElse' :
  25072. diff < -1 ? 'lastWeek' :
  25073. diff < 0 ? 'lastDay' :
  25074. diff < 1 ? 'sameDay' :
  25075. diff < 2 ? 'nextDay' :
  25076. diff < 7 ? 'nextWeek' : 'sameElse';
  25077. return this.format(this.lang().calendar(format, this));
  25078. },
  25079. isLeapYear : function () {
  25080. return isLeapYear(this.year());
  25081. },
  25082. isDST : function () {
  25083. return (this.zone() < this.clone().month(0).zone() ||
  25084. this.zone() < this.clone().month(5).zone());
  25085. },
  25086. day : function (input) {
  25087. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  25088. if (input != null) {
  25089. input = parseWeekday(input, this.lang());
  25090. return this.add({ d : input - day });
  25091. } else {
  25092. return day;
  25093. }
  25094. },
  25095. month : makeAccessor('Month', true),
  25096. startOf: function (units) {
  25097. units = normalizeUnits(units);
  25098. // the following switch intentionally omits break keywords
  25099. // to utilize falling through the cases.
  25100. switch (units) {
  25101. case 'year':
  25102. this.month(0);
  25103. /* falls through */
  25104. case 'quarter':
  25105. case 'month':
  25106. this.date(1);
  25107. /* falls through */
  25108. case 'week':
  25109. case 'isoWeek':
  25110. case 'day':
  25111. this.hours(0);
  25112. /* falls through */
  25113. case 'hour':
  25114. this.minutes(0);
  25115. /* falls through */
  25116. case 'minute':
  25117. this.seconds(0);
  25118. /* falls through */
  25119. case 'second':
  25120. this.milliseconds(0);
  25121. /* falls through */
  25122. }
  25123. // weeks are a special case
  25124. if (units === 'week') {
  25125. this.weekday(0);
  25126. } else if (units === 'isoWeek') {
  25127. this.isoWeekday(1);
  25128. }
  25129. // quarters are also special
  25130. if (units === 'quarter') {
  25131. this.month(Math.floor(this.month() / 3) * 3);
  25132. }
  25133. return this;
  25134. },
  25135. endOf: function (units) {
  25136. units = normalizeUnits(units);
  25137. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  25138. },
  25139. isAfter: function (input, units) {
  25140. units = typeof units !== 'undefined' ? units : 'millisecond';
  25141. return +this.clone().startOf(units) > +moment(input).startOf(units);
  25142. },
  25143. isBefore: function (input, units) {
  25144. units = typeof units !== 'undefined' ? units : 'millisecond';
  25145. return +this.clone().startOf(units) < +moment(input).startOf(units);
  25146. },
  25147. isSame: function (input, units) {
  25148. units = units || 'ms';
  25149. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  25150. },
  25151. min: deprecate(
  25152. "moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",
  25153. function (other) {
  25154. other = moment.apply(null, arguments);
  25155. return other < this ? this : other;
  25156. }
  25157. ),
  25158. max: deprecate(
  25159. "moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",
  25160. function (other) {
  25161. other = moment.apply(null, arguments);
  25162. return other > this ? this : other;
  25163. }
  25164. ),
  25165. // keepTime = true means only change the timezone, without affecting
  25166. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  25167. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  25168. // adjust the time as needed, to be valid.
  25169. //
  25170. // Keeping the time actually adds/subtracts (one hour)
  25171. // from the actual represented time. That is why we call updateOffset
  25172. // a second time. In case it wants us to change the offset again
  25173. // _changeInProgress == true case, then we have to adjust, because
  25174. // there is no such time in the given timezone.
  25175. zone : function (input, keepTime) {
  25176. var offset = this._offset || 0;
  25177. if (input != null) {
  25178. if (typeof input === "string") {
  25179. input = timezoneMinutesFromString(input);
  25180. }
  25181. if (Math.abs(input) < 16) {
  25182. input = input * 60;
  25183. }
  25184. this._offset = input;
  25185. this._isUTC = true;
  25186. if (offset !== input) {
  25187. if (!keepTime || this._changeInProgress) {
  25188. addOrSubtractDurationFromMoment(this,
  25189. moment.duration(offset - input, 'm'), 1, false);
  25190. } else if (!this._changeInProgress) {
  25191. this._changeInProgress = true;
  25192. moment.updateOffset(this, true);
  25193. this._changeInProgress = null;
  25194. }
  25195. }
  25196. } else {
  25197. return this._isUTC ? offset : this._d.getTimezoneOffset();
  25198. }
  25199. return this;
  25200. },
  25201. zoneAbbr : function () {
  25202. return this._isUTC ? "UTC" : "";
  25203. },
  25204. zoneName : function () {
  25205. return this._isUTC ? "Coordinated Universal Time" : "";
  25206. },
  25207. parseZone : function () {
  25208. if (this._tzm) {
  25209. this.zone(this._tzm);
  25210. } else if (typeof this._i === 'string') {
  25211. this.zone(this._i);
  25212. }
  25213. return this;
  25214. },
  25215. hasAlignedHourOffset : function (input) {
  25216. if (!input) {
  25217. input = 0;
  25218. }
  25219. else {
  25220. input = moment(input).zone();
  25221. }
  25222. return (this.zone() - input) % 60 === 0;
  25223. },
  25224. daysInMonth : function () {
  25225. return daysInMonth(this.year(), this.month());
  25226. },
  25227. dayOfYear : function (input) {
  25228. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  25229. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  25230. },
  25231. quarter : function (input) {
  25232. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  25233. },
  25234. weekYear : function (input) {
  25235. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  25236. return input == null ? year : this.add("y", (input - year));
  25237. },
  25238. isoWeekYear : function (input) {
  25239. var year = weekOfYear(this, 1, 4).year;
  25240. return input == null ? year : this.add("y", (input - year));
  25241. },
  25242. week : function (input) {
  25243. var week = this.lang().week(this);
  25244. return input == null ? week : this.add("d", (input - week) * 7);
  25245. },
  25246. isoWeek : function (input) {
  25247. var week = weekOfYear(this, 1, 4).week;
  25248. return input == null ? week : this.add("d", (input - week) * 7);
  25249. },
  25250. weekday : function (input) {
  25251. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  25252. return input == null ? weekday : this.add("d", input - weekday);
  25253. },
  25254. isoWeekday : function (input) {
  25255. // behaves the same as moment#day except
  25256. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  25257. // as a setter, sunday should belong to the previous week.
  25258. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  25259. },
  25260. isoWeeksInYear : function () {
  25261. return weeksInYear(this.year(), 1, 4);
  25262. },
  25263. weeksInYear : function () {
  25264. var weekInfo = this._lang._week;
  25265. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  25266. },
  25267. get : function (units) {
  25268. units = normalizeUnits(units);
  25269. return this[units]();
  25270. },
  25271. set : function (units, value) {
  25272. units = normalizeUnits(units);
  25273. if (typeof this[units] === 'function') {
  25274. this[units](value);
  25275. }
  25276. return this;
  25277. },
  25278. // If passed a language key, it will set the language for this
  25279. // instance. Otherwise, it will return the language configuration
  25280. // variables for this instance.
  25281. lang : function (key) {
  25282. if (key === undefined) {
  25283. return this._lang;
  25284. } else {
  25285. this._lang = getLangDefinition(key);
  25286. return this;
  25287. }
  25288. }
  25289. });
  25290. function rawMonthSetter(mom, value) {
  25291. var dayOfMonth;
  25292. // TODO: Move this out of here!
  25293. if (typeof value === 'string') {
  25294. value = mom.lang().monthsParse(value);
  25295. // TODO: Another silent failure?
  25296. if (typeof value !== 'number') {
  25297. return mom;
  25298. }
  25299. }
  25300. dayOfMonth = Math.min(mom.date(),
  25301. daysInMonth(mom.year(), value));
  25302. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  25303. return mom;
  25304. }
  25305. function rawGetter(mom, unit) {
  25306. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  25307. }
  25308. function rawSetter(mom, unit, value) {
  25309. if (unit === 'Month') {
  25310. return rawMonthSetter(mom, value);
  25311. } else {
  25312. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  25313. }
  25314. }
  25315. function makeAccessor(unit, keepTime) {
  25316. return function (value) {
  25317. if (value != null) {
  25318. rawSetter(this, unit, value);
  25319. moment.updateOffset(this, keepTime);
  25320. return this;
  25321. } else {
  25322. return rawGetter(this, unit);
  25323. }
  25324. };
  25325. }
  25326. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  25327. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  25328. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  25329. // Setting the hour should keep the time, because the user explicitly
  25330. // specified which hour he wants. So trying to maintain the same hour (in
  25331. // a new timezone) makes sense. Adding/subtracting hours does not follow
  25332. // this rule.
  25333. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  25334. // moment.fn.month is defined separately
  25335. moment.fn.date = makeAccessor('Date', true);
  25336. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  25337. moment.fn.year = makeAccessor('FullYear', true);
  25338. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  25339. // add plural methods
  25340. moment.fn.days = moment.fn.day;
  25341. moment.fn.months = moment.fn.month;
  25342. moment.fn.weeks = moment.fn.week;
  25343. moment.fn.isoWeeks = moment.fn.isoWeek;
  25344. moment.fn.quarters = moment.fn.quarter;
  25345. // add aliased format methods
  25346. moment.fn.toJSON = moment.fn.toISOString;
  25347. /************************************
  25348. Duration Prototype
  25349. ************************************/
  25350. extend(moment.duration.fn = Duration.prototype, {
  25351. _bubble : function () {
  25352. var milliseconds = this._milliseconds,
  25353. days = this._days,
  25354. months = this._months,
  25355. data = this._data,
  25356. seconds, minutes, hours, years;
  25357. // The following code bubbles up values, see the tests for
  25358. // examples of what that means.
  25359. data.milliseconds = milliseconds % 1000;
  25360. seconds = absRound(milliseconds / 1000);
  25361. data.seconds = seconds % 60;
  25362. minutes = absRound(seconds / 60);
  25363. data.minutes = minutes % 60;
  25364. hours = absRound(minutes / 60);
  25365. data.hours = hours % 24;
  25366. days += absRound(hours / 24);
  25367. data.days = days % 30;
  25368. months += absRound(days / 30);
  25369. data.months = months % 12;
  25370. years = absRound(months / 12);
  25371. data.years = years;
  25372. },
  25373. weeks : function () {
  25374. return absRound(this.days() / 7);
  25375. },
  25376. valueOf : function () {
  25377. return this._milliseconds +
  25378. this._days * 864e5 +
  25379. (this._months % 12) * 2592e6 +
  25380. toInt(this._months / 12) * 31536e6;
  25381. },
  25382. humanize : function (withSuffix) {
  25383. var difference = +this,
  25384. output = relativeTime(difference, !withSuffix, this.lang());
  25385. if (withSuffix) {
  25386. output = this.lang().pastFuture(difference, output);
  25387. }
  25388. return this.lang().postformat(output);
  25389. },
  25390. add : function (input, val) {
  25391. // supports only 2.0-style add(1, 's') or add(moment)
  25392. var dur = moment.duration(input, val);
  25393. this._milliseconds += dur._milliseconds;
  25394. this._days += dur._days;
  25395. this._months += dur._months;
  25396. this._bubble();
  25397. return this;
  25398. },
  25399. subtract : function (input, val) {
  25400. var dur = moment.duration(input, val);
  25401. this._milliseconds -= dur._milliseconds;
  25402. this._days -= dur._days;
  25403. this._months -= dur._months;
  25404. this._bubble();
  25405. return this;
  25406. },
  25407. get : function (units) {
  25408. units = normalizeUnits(units);
  25409. return this[units.toLowerCase() + 's']();
  25410. },
  25411. as : function (units) {
  25412. units = normalizeUnits(units);
  25413. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  25414. },
  25415. lang : moment.fn.lang,
  25416. toIsoString : function () {
  25417. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  25418. var years = Math.abs(this.years()),
  25419. months = Math.abs(this.months()),
  25420. days = Math.abs(this.days()),
  25421. hours = Math.abs(this.hours()),
  25422. minutes = Math.abs(this.minutes()),
  25423. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  25424. if (!this.asSeconds()) {
  25425. // this is the same as C#'s (Noda) and python (isodate)...
  25426. // but not other JS (goog.date)
  25427. return 'P0D';
  25428. }
  25429. return (this.asSeconds() < 0 ? '-' : '') +
  25430. 'P' +
  25431. (years ? years + 'Y' : '') +
  25432. (months ? months + 'M' : '') +
  25433. (days ? days + 'D' : '') +
  25434. ((hours || minutes || seconds) ? 'T' : '') +
  25435. (hours ? hours + 'H' : '') +
  25436. (minutes ? minutes + 'M' : '') +
  25437. (seconds ? seconds + 'S' : '');
  25438. }
  25439. });
  25440. function makeDurationGetter(name) {
  25441. moment.duration.fn[name] = function () {
  25442. return this._data[name];
  25443. };
  25444. }
  25445. function makeDurationAsGetter(name, factor) {
  25446. moment.duration.fn['as' + name] = function () {
  25447. return +this / factor;
  25448. };
  25449. }
  25450. for (i in unitMillisecondFactors) {
  25451. if (unitMillisecondFactors.hasOwnProperty(i)) {
  25452. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  25453. makeDurationGetter(i.toLowerCase());
  25454. }
  25455. }
  25456. makeDurationAsGetter('Weeks', 6048e5);
  25457. moment.duration.fn.asMonths = function () {
  25458. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  25459. };
  25460. /************************************
  25461. Default Lang
  25462. ************************************/
  25463. // Set default language, other languages will inherit from English.
  25464. moment.lang('en', {
  25465. ordinal : function (number) {
  25466. var b = number % 10,
  25467. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  25468. (b === 1) ? 'st' :
  25469. (b === 2) ? 'nd' :
  25470. (b === 3) ? 'rd' : 'th';
  25471. return number + output;
  25472. }
  25473. });
  25474. /* EMBED_LANGUAGES */
  25475. /************************************
  25476. Exposing Moment
  25477. ************************************/
  25478. function makeGlobal(shouldDeprecate) {
  25479. /*global ender:false */
  25480. if (typeof ender !== 'undefined') {
  25481. return;
  25482. }
  25483. oldGlobalMoment = globalScope.moment;
  25484. if (shouldDeprecate) {
  25485. globalScope.moment = deprecate(
  25486. "Accessing Moment through the global scope is " +
  25487. "deprecated, and will be removed in an upcoming " +
  25488. "release.",
  25489. moment);
  25490. } else {
  25491. globalScope.moment = moment;
  25492. }
  25493. }
  25494. // CommonJS module is defined
  25495. if (hasModule) {
  25496. module.exports = moment;
  25497. } else if (typeof define === "function" && define.amd) {
  25498. define("moment", function (require, exports, module) {
  25499. if (module.config && module.config() && module.config().noGlobal === true) {
  25500. // release the global variable
  25501. globalScope.moment = oldGlobalMoment;
  25502. }
  25503. return moment;
  25504. });
  25505. makeGlobal(true);
  25506. } else {
  25507. makeGlobal();
  25508. }
  25509. }).call(this);
  25510. },{}],5:[function(require,module,exports){
  25511. /**
  25512. * Copyright 2012 Craig Campbell
  25513. *
  25514. * Licensed under the Apache License, Version 2.0 (the "License");
  25515. * you may not use this file except in compliance with the License.
  25516. * You may obtain a copy of the License at
  25517. *
  25518. * http://www.apache.org/licenses/LICENSE-2.0
  25519. *
  25520. * Unless required by applicable law or agreed to in writing, software
  25521. * distributed under the License is distributed on an "AS IS" BASIS,
  25522. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  25523. * See the License for the specific language governing permissions and
  25524. * limitations under the License.
  25525. *
  25526. * Mousetrap is a simple keyboard shortcut library for Javascript with
  25527. * no external dependencies
  25528. *
  25529. * @version 1.1.2
  25530. * @url craig.is/killing/mice
  25531. */
  25532. /**
  25533. * mapping of special keycodes to their corresponding keys
  25534. *
  25535. * everything in this dictionary cannot use keypress events
  25536. * so it has to be here to map to the correct keycodes for
  25537. * keyup/keydown events
  25538. *
  25539. * @type {Object}
  25540. */
  25541. var _MAP = {
  25542. 8: 'backspace',
  25543. 9: 'tab',
  25544. 13: 'enter',
  25545. 16: 'shift',
  25546. 17: 'ctrl',
  25547. 18: 'alt',
  25548. 20: 'capslock',
  25549. 27: 'esc',
  25550. 32: 'space',
  25551. 33: 'pageup',
  25552. 34: 'pagedown',
  25553. 35: 'end',
  25554. 36: 'home',
  25555. 37: 'left',
  25556. 38: 'up',
  25557. 39: 'right',
  25558. 40: 'down',
  25559. 45: 'ins',
  25560. 46: 'del',
  25561. 91: 'meta',
  25562. 93: 'meta',
  25563. 224: 'meta'
  25564. },
  25565. /**
  25566. * mapping for special characters so they can support
  25567. *
  25568. * this dictionary is only used incase you want to bind a
  25569. * keyup or keydown event to one of these keys
  25570. *
  25571. * @type {Object}
  25572. */
  25573. _KEYCODE_MAP = {
  25574. 106: '*',
  25575. 107: '+',
  25576. 109: '-',
  25577. 110: '.',
  25578. 111 : '/',
  25579. 186: ';',
  25580. 187: '=',
  25581. 188: ',',
  25582. 189: '-',
  25583. 190: '.',
  25584. 191: '/',
  25585. 192: '`',
  25586. 219: '[',
  25587. 220: '\\',
  25588. 221: ']',
  25589. 222: '\''
  25590. },
  25591. /**
  25592. * this is a mapping of keys that require shift on a US keypad
  25593. * back to the non shift equivelents
  25594. *
  25595. * this is so you can use keyup events with these keys
  25596. *
  25597. * note that this will only work reliably on US keyboards
  25598. *
  25599. * @type {Object}
  25600. */
  25601. _SHIFT_MAP = {
  25602. '~': '`',
  25603. '!': '1',
  25604. '@': '2',
  25605. '#': '3',
  25606. '$': '4',
  25607. '%': '5',
  25608. '^': '6',
  25609. '&': '7',
  25610. '*': '8',
  25611. '(': '9',
  25612. ')': '0',
  25613. '_': '-',
  25614. '+': '=',
  25615. ':': ';',
  25616. '\"': '\'',
  25617. '<': ',',
  25618. '>': '.',
  25619. '?': '/',
  25620. '|': '\\'
  25621. },
  25622. /**
  25623. * this is a list of special strings you can use to map
  25624. * to modifier keys when you specify your keyboard shortcuts
  25625. *
  25626. * @type {Object}
  25627. */
  25628. _SPECIAL_ALIASES = {
  25629. 'option': 'alt',
  25630. 'command': 'meta',
  25631. 'return': 'enter',
  25632. 'escape': 'esc'
  25633. },
  25634. /**
  25635. * variable to store the flipped version of _MAP from above
  25636. * needed to check if we should use keypress or not when no action
  25637. * is specified
  25638. *
  25639. * @type {Object|undefined}
  25640. */
  25641. _REVERSE_MAP,
  25642. /**
  25643. * a list of all the callbacks setup via Mousetrap.bind()
  25644. *
  25645. * @type {Object}
  25646. */
  25647. _callbacks = {},
  25648. /**
  25649. * direct map of string combinations to callbacks used for trigger()
  25650. *
  25651. * @type {Object}
  25652. */
  25653. _direct_map = {},
  25654. /**
  25655. * keeps track of what level each sequence is at since multiple
  25656. * sequences can start out with the same sequence
  25657. *
  25658. * @type {Object}
  25659. */
  25660. _sequence_levels = {},
  25661. /**
  25662. * variable to store the setTimeout call
  25663. *
  25664. * @type {null|number}
  25665. */
  25666. _reset_timer,
  25667. /**
  25668. * temporary state where we will ignore the next keyup
  25669. *
  25670. * @type {boolean|string}
  25671. */
  25672. _ignore_next_keyup = false,
  25673. /**
  25674. * are we currently inside of a sequence?
  25675. * type of action ("keyup" or "keydown" or "keypress") or false
  25676. *
  25677. * @type {boolean|string}
  25678. */
  25679. _inside_sequence = false;
  25680. /**
  25681. * loop through the f keys, f1 to f19 and add them to the map
  25682. * programatically
  25683. */
  25684. for (var i = 1; i < 20; ++i) {
  25685. _MAP[111 + i] = 'f' + i;
  25686. }
  25687. /**
  25688. * loop through to map numbers on the numeric keypad
  25689. */
  25690. for (i = 0; i <= 9; ++i) {
  25691. _MAP[i + 96] = i;
  25692. }
  25693. /**
  25694. * cross browser add event method
  25695. *
  25696. * @param {Element|HTMLDocument} object
  25697. * @param {string} type
  25698. * @param {Function} callback
  25699. * @returns void
  25700. */
  25701. function _addEvent(object, type, callback) {
  25702. if (object.addEventListener) {
  25703. return object.addEventListener(type, callback, false);
  25704. }
  25705. object.attachEvent('on' + type, callback);
  25706. }
  25707. /**
  25708. * takes the event and returns the key character
  25709. *
  25710. * @param {Event} e
  25711. * @return {string}
  25712. */
  25713. function _characterFromEvent(e) {
  25714. // for keypress events we should return the character as is
  25715. if (e.type == 'keypress') {
  25716. return String.fromCharCode(e.which);
  25717. }
  25718. // for non keypress events the special maps are needed
  25719. if (_MAP[e.which]) {
  25720. return _MAP[e.which];
  25721. }
  25722. if (_KEYCODE_MAP[e.which]) {
  25723. return _KEYCODE_MAP[e.which];
  25724. }
  25725. // if it is not in the special map
  25726. return String.fromCharCode(e.which).toLowerCase();
  25727. }
  25728. /**
  25729. * should we stop this event before firing off callbacks
  25730. *
  25731. * @param {Event} e
  25732. * @return {boolean}
  25733. */
  25734. function _stop(e) {
  25735. var element = e.target || e.srcElement,
  25736. tag_name = element.tagName;
  25737. // if the element has the class "mousetrap" then no need to stop
  25738. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  25739. return false;
  25740. }
  25741. // stop for input, select, and textarea
  25742. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  25743. }
  25744. /**
  25745. * checks if two arrays are equal
  25746. *
  25747. * @param {Array} modifiers1
  25748. * @param {Array} modifiers2
  25749. * @returns {boolean}
  25750. */
  25751. function _modifiersMatch(modifiers1, modifiers2) {
  25752. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  25753. }
  25754. /**
  25755. * resets all sequence counters except for the ones passed in
  25756. *
  25757. * @param {Object} do_not_reset
  25758. * @returns void
  25759. */
  25760. function _resetSequences(do_not_reset) {
  25761. do_not_reset = do_not_reset || {};
  25762. var active_sequences = false,
  25763. key;
  25764. for (key in _sequence_levels) {
  25765. if (do_not_reset[key]) {
  25766. active_sequences = true;
  25767. continue;
  25768. }
  25769. _sequence_levels[key] = 0;
  25770. }
  25771. if (!active_sequences) {
  25772. _inside_sequence = false;
  25773. }
  25774. }
  25775. /**
  25776. * finds all callbacks that match based on the keycode, modifiers,
  25777. * and action
  25778. *
  25779. * @param {string} character
  25780. * @param {Array} modifiers
  25781. * @param {string} action
  25782. * @param {boolean=} remove - should we remove any matches
  25783. * @param {string=} combination
  25784. * @returns {Array}
  25785. */
  25786. function _getMatches(character, modifiers, action, remove, combination) {
  25787. var i,
  25788. callback,
  25789. matches = [];
  25790. // if there are no events related to this keycode
  25791. if (!_callbacks[character]) {
  25792. return [];
  25793. }
  25794. // if a modifier key is coming up on its own we should allow it
  25795. if (action == 'keyup' && _isModifier(character)) {
  25796. modifiers = [character];
  25797. }
  25798. // loop through all callbacks for the key that was pressed
  25799. // and see if any of them match
  25800. for (i = 0; i < _callbacks[character].length; ++i) {
  25801. callback = _callbacks[character][i];
  25802. // if this is a sequence but it is not at the right level
  25803. // then move onto the next match
  25804. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  25805. continue;
  25806. }
  25807. // if the action we are looking for doesn't match the action we got
  25808. // then we should keep going
  25809. if (action != callback.action) {
  25810. continue;
  25811. }
  25812. // if this is a keypress event that means that we need to only
  25813. // look at the character, otherwise check the modifiers as
  25814. // well
  25815. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  25816. // remove is used so if you change your mind and call bind a
  25817. // second time with a new function the first one is overwritten
  25818. if (remove && callback.combo == combination) {
  25819. _callbacks[character].splice(i, 1);
  25820. }
  25821. matches.push(callback);
  25822. }
  25823. }
  25824. return matches;
  25825. }
  25826. /**
  25827. * takes a key event and figures out what the modifiers are
  25828. *
  25829. * @param {Event} e
  25830. * @returns {Array}
  25831. */
  25832. function _eventModifiers(e) {
  25833. var modifiers = [];
  25834. if (e.shiftKey) {
  25835. modifiers.push('shift');
  25836. }
  25837. if (e.altKey) {
  25838. modifiers.push('alt');
  25839. }
  25840. if (e.ctrlKey) {
  25841. modifiers.push('ctrl');
  25842. }
  25843. if (e.metaKey) {
  25844. modifiers.push('meta');
  25845. }
  25846. return modifiers;
  25847. }
  25848. /**
  25849. * actually calls the callback function
  25850. *
  25851. * if your callback function returns false this will use the jquery
  25852. * convention - prevent default and stop propogation on the event
  25853. *
  25854. * @param {Function} callback
  25855. * @param {Event} e
  25856. * @returns void
  25857. */
  25858. function _fireCallback(callback, e) {
  25859. if (callback(e) === false) {
  25860. if (e.preventDefault) {
  25861. e.preventDefault();
  25862. }
  25863. if (e.stopPropagation) {
  25864. e.stopPropagation();
  25865. }
  25866. e.returnValue = false;
  25867. e.cancelBubble = true;
  25868. }
  25869. }
  25870. /**
  25871. * handles a character key event
  25872. *
  25873. * @param {string} character
  25874. * @param {Event} e
  25875. * @returns void
  25876. */
  25877. function _handleCharacter(character, e) {
  25878. // if this event should not happen stop here
  25879. if (_stop(e)) {
  25880. return;
  25881. }
  25882. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  25883. i,
  25884. do_not_reset = {},
  25885. processed_sequence_callback = false;
  25886. // loop through matching callbacks for this key event
  25887. for (i = 0; i < callbacks.length; ++i) {
  25888. // fire for all sequence callbacks
  25889. // this is because if for example you have multiple sequences
  25890. // bound such as "g i" and "g t" they both need to fire the
  25891. // callback for matching g cause otherwise you can only ever
  25892. // match the first one
  25893. if (callbacks[i].seq) {
  25894. processed_sequence_callback = true;
  25895. // keep a list of which sequences were matches for later
  25896. do_not_reset[callbacks[i].seq] = 1;
  25897. _fireCallback(callbacks[i].callback, e);
  25898. continue;
  25899. }
  25900. // if there were no sequence matches but we are still here
  25901. // that means this is a regular match so we should fire that
  25902. if (!processed_sequence_callback && !_inside_sequence) {
  25903. _fireCallback(callbacks[i].callback, e);
  25904. }
  25905. }
  25906. // if you are inside of a sequence and the key you are pressing
  25907. // is not a modifier key then we should reset all sequences
  25908. // that were not matched by this key event
  25909. if (e.type == _inside_sequence && !_isModifier(character)) {
  25910. _resetSequences(do_not_reset);
  25911. }
  25912. }
  25913. /**
  25914. * handles a keydown event
  25915. *
  25916. * @param {Event} e
  25917. * @returns void
  25918. */
  25919. function _handleKey(e) {
  25920. // normalize e.which for key events
  25921. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  25922. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  25923. var character = _characterFromEvent(e);
  25924. // no character found then stop
  25925. if (!character) {
  25926. return;
  25927. }
  25928. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  25929. _ignore_next_keyup = false;
  25930. return;
  25931. }
  25932. _handleCharacter(character, e);
  25933. }
  25934. /**
  25935. * determines if the keycode specified is a modifier key or not
  25936. *
  25937. * @param {string} key
  25938. * @returns {boolean}
  25939. */
  25940. function _isModifier(key) {
  25941. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  25942. }
  25943. /**
  25944. * called to set a 1 second timeout on the specified sequence
  25945. *
  25946. * this is so after each key press in the sequence you have 1 second
  25947. * to press the next key before you have to start over
  25948. *
  25949. * @returns void
  25950. */
  25951. function _resetSequenceTimer() {
  25952. clearTimeout(_reset_timer);
  25953. _reset_timer = setTimeout(_resetSequences, 1000);
  25954. }
  25955. /**
  25956. * reverses the map lookup so that we can look for specific keys
  25957. * to see what can and can't use keypress
  25958. *
  25959. * @return {Object}
  25960. */
  25961. function _getReverseMap() {
  25962. if (!_REVERSE_MAP) {
  25963. _REVERSE_MAP = {};
  25964. for (var key in _MAP) {
  25965. // pull out the numeric keypad from here cause keypress should
  25966. // be able to detect the keys from the character
  25967. if (key > 95 && key < 112) {
  25968. continue;
  25969. }
  25970. if (_MAP.hasOwnProperty(key)) {
  25971. _REVERSE_MAP[_MAP[key]] = key;
  25972. }
  25973. }
  25974. }
  25975. return _REVERSE_MAP;
  25976. }
  25977. /**
  25978. * picks the best action based on the key combination
  25979. *
  25980. * @param {string} key - character for key
  25981. * @param {Array} modifiers
  25982. * @param {string=} action passed in
  25983. */
  25984. function _pickBestAction(key, modifiers, action) {
  25985. // if no action was picked in we should try to pick the one
  25986. // that we think would work best for this key
  25987. if (!action) {
  25988. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  25989. }
  25990. // modifier keys don't work as expected with keypress,
  25991. // switch to keydown
  25992. if (action == 'keypress' && modifiers.length) {
  25993. action = 'keydown';
  25994. }
  25995. return action;
  25996. }
  25997. /**
  25998. * binds a key sequence to an event
  25999. *
  26000. * @param {string} combo - combo specified in bind call
  26001. * @param {Array} keys
  26002. * @param {Function} callback
  26003. * @param {string=} action
  26004. * @returns void
  26005. */
  26006. function _bindSequence(combo, keys, callback, action) {
  26007. // start off by adding a sequence level record for this combination
  26008. // and setting the level to 0
  26009. _sequence_levels[combo] = 0;
  26010. // if there is no action pick the best one for the first key
  26011. // in the sequence
  26012. if (!action) {
  26013. action = _pickBestAction(keys[0], []);
  26014. }
  26015. /**
  26016. * callback to increase the sequence level for this sequence and reset
  26017. * all other sequences that were active
  26018. *
  26019. * @param {Event} e
  26020. * @returns void
  26021. */
  26022. var _increaseSequence = function(e) {
  26023. _inside_sequence = action;
  26024. ++_sequence_levels[combo];
  26025. _resetSequenceTimer();
  26026. },
  26027. /**
  26028. * wraps the specified callback inside of another function in order
  26029. * to reset all sequence counters as soon as this sequence is done
  26030. *
  26031. * @param {Event} e
  26032. * @returns void
  26033. */
  26034. _callbackAndReset = function(e) {
  26035. _fireCallback(callback, e);
  26036. // we should ignore the next key up if the action is key down
  26037. // or keypress. this is so if you finish a sequence and
  26038. // release the key the final key will not trigger a keyup
  26039. if (action !== 'keyup') {
  26040. _ignore_next_keyup = _characterFromEvent(e);
  26041. }
  26042. // weird race condition if a sequence ends with the key
  26043. // another sequence begins with
  26044. setTimeout(_resetSequences, 10);
  26045. },
  26046. i;
  26047. // loop through keys one at a time and bind the appropriate callback
  26048. // function. for any key leading up to the final one it should
  26049. // increase the sequence. after the final, it should reset all sequences
  26050. for (i = 0; i < keys.length; ++i) {
  26051. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  26052. }
  26053. }
  26054. /**
  26055. * binds a single keyboard combination
  26056. *
  26057. * @param {string} combination
  26058. * @param {Function} callback
  26059. * @param {string=} action
  26060. * @param {string=} sequence_name - name of sequence if part of sequence
  26061. * @param {number=} level - what part of the sequence the command is
  26062. * @returns void
  26063. */
  26064. function _bindSingle(combination, callback, action, sequence_name, level) {
  26065. // make sure multiple spaces in a row become a single space
  26066. combination = combination.replace(/\s+/g, ' ');
  26067. var sequence = combination.split(' '),
  26068. i,
  26069. key,
  26070. keys,
  26071. modifiers = [];
  26072. // if this pattern is a sequence of keys then run through this method
  26073. // to reprocess each pattern one key at a time
  26074. if (sequence.length > 1) {
  26075. return _bindSequence(combination, sequence, callback, action);
  26076. }
  26077. // take the keys from this pattern and figure out what the actual
  26078. // pattern is all about
  26079. keys = combination === '+' ? ['+'] : combination.split('+');
  26080. for (i = 0; i < keys.length; ++i) {
  26081. key = keys[i];
  26082. // normalize key names
  26083. if (_SPECIAL_ALIASES[key]) {
  26084. key = _SPECIAL_ALIASES[key];
  26085. }
  26086. // if this is not a keypress event then we should
  26087. // be smart about using shift keys
  26088. // this will only work for US keyboards however
  26089. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  26090. key = _SHIFT_MAP[key];
  26091. modifiers.push('shift');
  26092. }
  26093. // if this key is a modifier then add it to the list of modifiers
  26094. if (_isModifier(key)) {
  26095. modifiers.push(key);
  26096. }
  26097. }
  26098. // depending on what the key combination is
  26099. // we will try to pick the best event for it
  26100. action = _pickBestAction(key, modifiers, action);
  26101. // make sure to initialize array if this is the first time
  26102. // a callback is added for this key
  26103. if (!_callbacks[key]) {
  26104. _callbacks[key] = [];
  26105. }
  26106. // remove an existing match if there is one
  26107. _getMatches(key, modifiers, action, !sequence_name, combination);
  26108. // add this call back to the array
  26109. // if it is a sequence put it at the beginning
  26110. // if not put it at the end
  26111. //
  26112. // this is important because the way these are processed expects
  26113. // the sequence ones to come first
  26114. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  26115. callback: callback,
  26116. modifiers: modifiers,
  26117. action: action,
  26118. seq: sequence_name,
  26119. level: level,
  26120. combo: combination
  26121. });
  26122. }
  26123. /**
  26124. * binds multiple combinations to the same callback
  26125. *
  26126. * @param {Array} combinations
  26127. * @param {Function} callback
  26128. * @param {string|undefined} action
  26129. * @returns void
  26130. */
  26131. function _bindMultiple(combinations, callback, action) {
  26132. for (var i = 0; i < combinations.length; ++i) {
  26133. _bindSingle(combinations[i], callback, action);
  26134. }
  26135. }
  26136. // start!
  26137. _addEvent(document, 'keypress', _handleKey);
  26138. _addEvent(document, 'keydown', _handleKey);
  26139. _addEvent(document, 'keyup', _handleKey);
  26140. var mousetrap = {
  26141. /**
  26142. * binds an event to mousetrap
  26143. *
  26144. * can be a single key, a combination of keys separated with +,
  26145. * a comma separated list of keys, an array of keys, or
  26146. * a sequence of keys separated by spaces
  26147. *
  26148. * be sure to list the modifier keys first to make sure that the
  26149. * correct key ends up getting bound (the last key in the pattern)
  26150. *
  26151. * @param {string|Array} keys
  26152. * @param {Function} callback
  26153. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  26154. * @returns void
  26155. */
  26156. bind: function(keys, callback, action) {
  26157. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  26158. _direct_map[keys + ':' + action] = callback;
  26159. return this;
  26160. },
  26161. /**
  26162. * unbinds an event to mousetrap
  26163. *
  26164. * the unbinding sets the callback function of the specified key combo
  26165. * to an empty function and deletes the corresponding key in the
  26166. * _direct_map dict.
  26167. *
  26168. * the keycombo+action has to be exactly the same as
  26169. * it was defined in the bind method
  26170. *
  26171. * TODO: actually remove this from the _callbacks dictionary instead
  26172. * of binding an empty function
  26173. *
  26174. * @param {string|Array} keys
  26175. * @param {string} action
  26176. * @returns void
  26177. */
  26178. unbind: function(keys, action) {
  26179. if (_direct_map[keys + ':' + action]) {
  26180. delete _direct_map[keys + ':' + action];
  26181. this.bind(keys, function() {}, action);
  26182. }
  26183. return this;
  26184. },
  26185. /**
  26186. * triggers an event that has already been bound
  26187. *
  26188. * @param {string} keys
  26189. * @param {string=} action
  26190. * @returns void
  26191. */
  26192. trigger: function(keys, action) {
  26193. _direct_map[keys + ':' + action]();
  26194. return this;
  26195. },
  26196. /**
  26197. * resets the library back to its initial state. this is useful
  26198. * if you want to clear out the current keyboard shortcuts and bind
  26199. * new ones - for example if you switch to another page
  26200. *
  26201. * @returns void
  26202. */
  26203. reset: function() {
  26204. _callbacks = {};
  26205. _direct_map = {};
  26206. return this;
  26207. }
  26208. };
  26209. module.exports = mousetrap;
  26210. },{}]},{},[1])
  26211. (1)
  26212. });