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.

30186 lines
904 KiB

  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 3.0.0
  8. * @date 2014-07-07
  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. var 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. * Returns the DataSet itself. Is overwritten for example by the DataView,
  2026. * which returns the DataSet it is connected to instead.
  2027. */
  2028. DataSet.prototype.getDataSet = function () {
  2029. return this;
  2030. };
  2031. /**
  2032. * Execute a callback function for every item in the dataset.
  2033. * @param {function} callback
  2034. * @param {Object} [options] Available options:
  2035. * {Object.<String, String>} [type]
  2036. * {String[]} [fields] filter fields
  2037. * {function} [filter] filter items
  2038. * {String | function} [order] Order the items by
  2039. * a field name or custom sort function.
  2040. */
  2041. DataSet.prototype.forEach = function (callback, options) {
  2042. var filter = options && options.filter,
  2043. type = options && options.type || this._options.type,
  2044. data = this._data,
  2045. item,
  2046. id;
  2047. if (options && options.order) {
  2048. // execute forEach on ordered list
  2049. var items = this.get(options);
  2050. for (var i = 0, len = items.length; i < len; i++) {
  2051. item = items[i];
  2052. id = item[this._fieldId];
  2053. callback(item, id);
  2054. }
  2055. }
  2056. else {
  2057. // unordered
  2058. for (id in data) {
  2059. if (data.hasOwnProperty(id)) {
  2060. item = this._getItem(id, type);
  2061. if (!filter || filter(item)) {
  2062. callback(item, id);
  2063. }
  2064. }
  2065. }
  2066. }
  2067. };
  2068. /**
  2069. * Map every item in the dataset.
  2070. * @param {function} callback
  2071. * @param {Object} [options] Available options:
  2072. * {Object.<String, String>} [type]
  2073. * {String[]} [fields] filter fields
  2074. * {function} [filter] filter items
  2075. * {String | function} [order] Order the items by
  2076. * a field name or custom sort function.
  2077. * @return {Object[]} mappedItems
  2078. */
  2079. DataSet.prototype.map = function (callback, options) {
  2080. var filter = options && options.filter,
  2081. type = options && options.type || this._options.type,
  2082. mappedItems = [],
  2083. data = this._data,
  2084. item;
  2085. // convert and filter items
  2086. for (var id in data) {
  2087. if (data.hasOwnProperty(id)) {
  2088. item = this._getItem(id, type);
  2089. if (!filter || filter(item)) {
  2090. mappedItems.push(callback(item, id));
  2091. }
  2092. }
  2093. }
  2094. // order items
  2095. if (options && options.order) {
  2096. this._sort(mappedItems, options.order);
  2097. }
  2098. return mappedItems;
  2099. };
  2100. /**
  2101. * Filter the fields of an item
  2102. * @param {Object} item
  2103. * @param {String[]} fields Field names
  2104. * @return {Object} filteredItem
  2105. * @private
  2106. */
  2107. DataSet.prototype._filterFields = function (item, fields) {
  2108. var filteredItem = {};
  2109. for (var field in item) {
  2110. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  2111. filteredItem[field] = item[field];
  2112. }
  2113. }
  2114. return filteredItem;
  2115. };
  2116. /**
  2117. * Sort the provided array with items
  2118. * @param {Object[]} items
  2119. * @param {String | function} order A field name or custom sort function.
  2120. * @private
  2121. */
  2122. DataSet.prototype._sort = function (items, order) {
  2123. if (util.isString(order)) {
  2124. // order by provided field name
  2125. var name = order; // field name
  2126. items.sort(function (a, b) {
  2127. var av = a[name];
  2128. var bv = b[name];
  2129. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  2130. });
  2131. }
  2132. else if (typeof order === 'function') {
  2133. // order by sort function
  2134. items.sort(order);
  2135. }
  2136. // TODO: extend order by an Object {field:String, direction:String}
  2137. // where direction can be 'asc' or 'desc'
  2138. else {
  2139. throw new TypeError('Order must be a function or a string');
  2140. }
  2141. };
  2142. /**
  2143. * Remove an object by pointer or by id
  2144. * @param {String | Number | Object | Array} id Object or id, or an array with
  2145. * objects or ids to be removed
  2146. * @param {String} [senderId] Optional sender id
  2147. * @return {Array} removedIds
  2148. */
  2149. DataSet.prototype.remove = function (id, senderId) {
  2150. var removedIds = [],
  2151. i, len, removedId;
  2152. if (Array.isArray(id)) {
  2153. for (i = 0, len = id.length; i < len; i++) {
  2154. removedId = this._remove(id[i]);
  2155. if (removedId != null) {
  2156. removedIds.push(removedId);
  2157. }
  2158. }
  2159. }
  2160. else {
  2161. removedId = this._remove(id);
  2162. if (removedId != null) {
  2163. removedIds.push(removedId);
  2164. }
  2165. }
  2166. if (removedIds.length) {
  2167. this._trigger('remove', {items: removedIds}, senderId);
  2168. }
  2169. return removedIds;
  2170. };
  2171. /**
  2172. * Remove an item by its id
  2173. * @param {Number | String | Object} id id or item
  2174. * @returns {Number | String | null} id
  2175. * @private
  2176. */
  2177. DataSet.prototype._remove = function (id) {
  2178. if (util.isNumber(id) || util.isString(id)) {
  2179. if (this._data[id]) {
  2180. delete this._data[id];
  2181. return id;
  2182. }
  2183. }
  2184. else if (id instanceof Object) {
  2185. var itemId = id[this._fieldId];
  2186. if (itemId && this._data[itemId]) {
  2187. delete this._data[itemId];
  2188. return itemId;
  2189. }
  2190. }
  2191. return null;
  2192. };
  2193. /**
  2194. * Clear the data
  2195. * @param {String} [senderId] Optional sender id
  2196. * @return {Array} removedIds The ids of all removed items
  2197. */
  2198. DataSet.prototype.clear = function (senderId) {
  2199. var ids = Object.keys(this._data);
  2200. this._data = {};
  2201. this._trigger('remove', {items: ids}, senderId);
  2202. return ids;
  2203. };
  2204. /**
  2205. * Find the item with maximum value of a specified field
  2206. * @param {String} field
  2207. * @return {Object | null} item Item containing max value, or null if no items
  2208. */
  2209. DataSet.prototype.max = function (field) {
  2210. var data = this._data,
  2211. max = null,
  2212. maxField = null;
  2213. for (var id in data) {
  2214. if (data.hasOwnProperty(id)) {
  2215. var item = data[id];
  2216. var itemField = item[field];
  2217. if (itemField != null && (!max || itemField > maxField)) {
  2218. max = item;
  2219. maxField = itemField;
  2220. }
  2221. }
  2222. }
  2223. return max;
  2224. };
  2225. /**
  2226. * Find the item with minimum value of a specified field
  2227. * @param {String} field
  2228. * @return {Object | null} item Item containing max value, or null if no items
  2229. */
  2230. DataSet.prototype.min = function (field) {
  2231. var data = this._data,
  2232. min = null,
  2233. minField = null;
  2234. for (var id in data) {
  2235. if (data.hasOwnProperty(id)) {
  2236. var item = data[id];
  2237. var itemField = item[field];
  2238. if (itemField != null && (!min || itemField < minField)) {
  2239. min = item;
  2240. minField = itemField;
  2241. }
  2242. }
  2243. }
  2244. return min;
  2245. };
  2246. /**
  2247. * Find all distinct values of a specified field
  2248. * @param {String} field
  2249. * @return {Array} values Array containing all distinct values. If data items
  2250. * do not contain the specified field are ignored.
  2251. * The returned array is unordered.
  2252. */
  2253. DataSet.prototype.distinct = function (field) {
  2254. var data = this._data;
  2255. var values = [];
  2256. var fieldType = this._options.type && this._options.type[field] || null;
  2257. var count = 0;
  2258. var i;
  2259. for (var prop in data) {
  2260. if (data.hasOwnProperty(prop)) {
  2261. var item = data[prop];
  2262. var value = item[field];
  2263. var exists = false;
  2264. for (i = 0; i < count; i++) {
  2265. if (values[i] == value) {
  2266. exists = true;
  2267. break;
  2268. }
  2269. }
  2270. if (!exists && (value !== undefined)) {
  2271. values[count] = value;
  2272. count++;
  2273. }
  2274. }
  2275. }
  2276. if (fieldType) {
  2277. for (i = 0; i < values.length; i++) {
  2278. values[i] = util.convert(values[i], fieldType);
  2279. }
  2280. }
  2281. return values;
  2282. };
  2283. /**
  2284. * Add a single item. Will fail when an item with the same id already exists.
  2285. * @param {Object} item
  2286. * @return {String} id
  2287. * @private
  2288. */
  2289. DataSet.prototype._addItem = function (item) {
  2290. var id = item[this._fieldId];
  2291. if (id != undefined) {
  2292. // check whether this id is already taken
  2293. if (this._data[id]) {
  2294. // item already exists
  2295. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  2296. }
  2297. }
  2298. else {
  2299. // generate an id
  2300. id = util.randomUUID();
  2301. item[this._fieldId] = id;
  2302. }
  2303. var d = {};
  2304. for (var field in item) {
  2305. if (item.hasOwnProperty(field)) {
  2306. var fieldType = this._type[field]; // type may be undefined
  2307. d[field] = util.convert(item[field], fieldType);
  2308. }
  2309. }
  2310. this._data[id] = d;
  2311. return id;
  2312. };
  2313. /**
  2314. * Get an item. Fields can be converted to a specific type
  2315. * @param {String} id
  2316. * @param {Object.<String, String>} [types] field types to convert
  2317. * @return {Object | null} item
  2318. * @private
  2319. */
  2320. DataSet.prototype._getItem = function (id, types) {
  2321. var field, value;
  2322. // get the item from the dataset
  2323. var raw = this._data[id];
  2324. if (!raw) {
  2325. return null;
  2326. }
  2327. // convert the items field types
  2328. var converted = {};
  2329. if (types) {
  2330. for (field in raw) {
  2331. if (raw.hasOwnProperty(field)) {
  2332. value = raw[field];
  2333. converted[field] = util.convert(value, types[field]);
  2334. }
  2335. }
  2336. }
  2337. else {
  2338. // no field types specified, no converting needed
  2339. for (field in raw) {
  2340. if (raw.hasOwnProperty(field)) {
  2341. value = raw[field];
  2342. converted[field] = value;
  2343. }
  2344. }
  2345. }
  2346. return converted;
  2347. };
  2348. /**
  2349. * Update a single item: merge with existing item.
  2350. * Will fail when the item has no id, or when there does not exist an item
  2351. * with the same id.
  2352. * @param {Object} item
  2353. * @return {String} id
  2354. * @private
  2355. */
  2356. DataSet.prototype._updateItem = function (item) {
  2357. var id = item[this._fieldId];
  2358. if (id == undefined) {
  2359. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  2360. }
  2361. var d = this._data[id];
  2362. if (!d) {
  2363. // item doesn't exist
  2364. throw new Error('Cannot update item: no item with id ' + id + ' found');
  2365. }
  2366. // merge with current item
  2367. for (var field in item) {
  2368. if (item.hasOwnProperty(field)) {
  2369. var fieldType = this._type[field]; // type may be undefined
  2370. d[field] = util.convert(item[field], fieldType);
  2371. }
  2372. }
  2373. return id;
  2374. };
  2375. /**
  2376. * Get an array with the column names of a Google DataTable
  2377. * @param {DataTable} dataTable
  2378. * @return {String[]} columnNames
  2379. * @private
  2380. */
  2381. DataSet.prototype._getColumnNames = function (dataTable) {
  2382. var columns = [];
  2383. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  2384. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  2385. }
  2386. return columns;
  2387. };
  2388. /**
  2389. * Append an item as a row to the dataTable
  2390. * @param dataTable
  2391. * @param columns
  2392. * @param item
  2393. * @private
  2394. */
  2395. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  2396. var row = dataTable.addRow();
  2397. for (var col = 0, cols = columns.length; col < cols; col++) {
  2398. var field = columns[col];
  2399. dataTable.setValue(row, col, item[field]);
  2400. }
  2401. };
  2402. /**
  2403. * DataView
  2404. *
  2405. * a dataview offers a filtered view on a dataset or an other dataview.
  2406. *
  2407. * @param {DataSet | DataView} data
  2408. * @param {Object} [options] Available options: see method get
  2409. *
  2410. * @constructor DataView
  2411. */
  2412. function DataView (data, options) {
  2413. this._data = null;
  2414. this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
  2415. this._options = options || {};
  2416. this._fieldId = 'id'; // name of the field containing id
  2417. this._subscribers = {}; // event subscribers
  2418. var me = this;
  2419. this.listener = function () {
  2420. me._onEvent.apply(me, arguments);
  2421. };
  2422. this.setData(data);
  2423. }
  2424. // TODO: implement a function .config() to dynamically update things like configured filter
  2425. // and trigger changes accordingly
  2426. /**
  2427. * Set a data source for the view
  2428. * @param {DataSet | DataView} data
  2429. */
  2430. DataView.prototype.setData = function (data) {
  2431. var ids, i, len;
  2432. if (this._data) {
  2433. // unsubscribe from current dataset
  2434. if (this._data.unsubscribe) {
  2435. this._data.unsubscribe('*', this.listener);
  2436. }
  2437. // trigger a remove of all items in memory
  2438. ids = [];
  2439. for (var id in this._ids) {
  2440. if (this._ids.hasOwnProperty(id)) {
  2441. ids.push(id);
  2442. }
  2443. }
  2444. this._ids = {};
  2445. this._trigger('remove', {items: ids});
  2446. }
  2447. this._data = data;
  2448. if (this._data) {
  2449. // update fieldId
  2450. this._fieldId = this._options.fieldId ||
  2451. (this._data && this._data.options && this._data.options.fieldId) ||
  2452. 'id';
  2453. // trigger an add of all added items
  2454. ids = this._data.getIds({filter: this._options && this._options.filter});
  2455. for (i = 0, len = ids.length; i < len; i++) {
  2456. id = ids[i];
  2457. this._ids[id] = true;
  2458. }
  2459. this._trigger('add', {items: ids});
  2460. // subscribe to new dataset
  2461. if (this._data.on) {
  2462. this._data.on('*', this.listener);
  2463. }
  2464. }
  2465. };
  2466. /**
  2467. * Get data from the data view
  2468. *
  2469. * Usage:
  2470. *
  2471. * get()
  2472. * get(options: Object)
  2473. * get(options: Object, data: Array | DataTable)
  2474. *
  2475. * get(id: Number)
  2476. * get(id: Number, options: Object)
  2477. * get(id: Number, options: Object, data: Array | DataTable)
  2478. *
  2479. * get(ids: Number[])
  2480. * get(ids: Number[], options: Object)
  2481. * get(ids: Number[], options: Object, data: Array | DataTable)
  2482. *
  2483. * Where:
  2484. *
  2485. * {Number | String} id The id of an item
  2486. * {Number[] | String{}} ids An array with ids of items
  2487. * {Object} options An Object with options. Available options:
  2488. * {String} [type] Type of data to be returned. Can
  2489. * be 'DataTable' or 'Array' (default)
  2490. * {Object.<String, String>} [convert]
  2491. * {String[]} [fields] field names to be returned
  2492. * {function} [filter] filter items
  2493. * {String | function} [order] Order the items by
  2494. * a field name or custom sort function.
  2495. * {Array | DataTable} [data] If provided, items will be appended to this
  2496. * array or table. Required in case of Google
  2497. * DataTable.
  2498. * @param args
  2499. */
  2500. DataView.prototype.get = function (args) {
  2501. var me = this;
  2502. // parse the arguments
  2503. var ids, options, data;
  2504. var firstType = util.getType(arguments[0]);
  2505. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2506. // get(id(s) [, options] [, data])
  2507. ids = arguments[0]; // can be a single id or an array with ids
  2508. options = arguments[1];
  2509. data = arguments[2];
  2510. }
  2511. else {
  2512. // get([, options] [, data])
  2513. options = arguments[0];
  2514. data = arguments[1];
  2515. }
  2516. // extend the options with the default options and provided options
  2517. var viewOptions = util.extend({}, this._options, options);
  2518. // create a combined filter method when needed
  2519. if (this._options.filter && options && options.filter) {
  2520. viewOptions.filter = function (item) {
  2521. return me._options.filter(item) && options.filter(item);
  2522. }
  2523. }
  2524. // build up the call to the linked data set
  2525. var getArguments = [];
  2526. if (ids != undefined) {
  2527. getArguments.push(ids);
  2528. }
  2529. getArguments.push(viewOptions);
  2530. getArguments.push(data);
  2531. return this._data && this._data.get.apply(this._data, getArguments);
  2532. };
  2533. /**
  2534. * Get ids of all items or from a filtered set of items.
  2535. * @param {Object} [options] An Object with options. Available options:
  2536. * {function} [filter] filter items
  2537. * {String | function} [order] Order the items by
  2538. * a field name or custom sort function.
  2539. * @return {Array} ids
  2540. */
  2541. DataView.prototype.getIds = function (options) {
  2542. var ids;
  2543. if (this._data) {
  2544. var defaultFilter = this._options.filter;
  2545. var filter;
  2546. if (options && options.filter) {
  2547. if (defaultFilter) {
  2548. filter = function (item) {
  2549. return defaultFilter(item) && options.filter(item);
  2550. }
  2551. }
  2552. else {
  2553. filter = options.filter;
  2554. }
  2555. }
  2556. else {
  2557. filter = defaultFilter;
  2558. }
  2559. ids = this._data.getIds({
  2560. filter: filter,
  2561. order: options && options.order
  2562. });
  2563. }
  2564. else {
  2565. ids = [];
  2566. }
  2567. return ids;
  2568. };
  2569. /**
  2570. * Get the DataSet to which this DataView is connected. In case there is a chain
  2571. * of multiple DataViews, the root DataSet of this chain is returned.
  2572. * @return {DataSet} dataSet
  2573. */
  2574. DataView.prototype.getDataSet = function () {
  2575. var dataSet = this;
  2576. while (dataSet instanceof DataView) {
  2577. dataSet = dataSet._data;
  2578. }
  2579. return dataSet || null;
  2580. };
  2581. /**
  2582. * Event listener. Will propagate all events from the connected data set to
  2583. * the subscribers of the DataView, but will filter the items and only trigger
  2584. * when there are changes in the filtered data set.
  2585. * @param {String} event
  2586. * @param {Object | null} params
  2587. * @param {String} senderId
  2588. * @private
  2589. */
  2590. DataView.prototype._onEvent = function (event, params, senderId) {
  2591. var i, len, id, item,
  2592. ids = params && params.items,
  2593. data = this._data,
  2594. added = [],
  2595. updated = [],
  2596. removed = [];
  2597. if (ids && data) {
  2598. switch (event) {
  2599. case 'add':
  2600. // filter the ids of the added items
  2601. for (i = 0, len = ids.length; i < len; i++) {
  2602. id = ids[i];
  2603. item = this.get(id);
  2604. if (item) {
  2605. this._ids[id] = true;
  2606. added.push(id);
  2607. }
  2608. }
  2609. break;
  2610. case 'update':
  2611. // determine the event from the views viewpoint: an updated
  2612. // item can be added, updated, or removed from this view.
  2613. for (i = 0, len = ids.length; i < len; i++) {
  2614. id = ids[i];
  2615. item = this.get(id);
  2616. if (item) {
  2617. if (this._ids[id]) {
  2618. updated.push(id);
  2619. }
  2620. else {
  2621. this._ids[id] = true;
  2622. added.push(id);
  2623. }
  2624. }
  2625. else {
  2626. if (this._ids[id]) {
  2627. delete this._ids[id];
  2628. removed.push(id);
  2629. }
  2630. else {
  2631. // nothing interesting for me :-(
  2632. }
  2633. }
  2634. }
  2635. break;
  2636. case 'remove':
  2637. // filter the ids of the removed items
  2638. for (i = 0, len = ids.length; i < len; i++) {
  2639. id = ids[i];
  2640. if (this._ids[id]) {
  2641. delete this._ids[id];
  2642. removed.push(id);
  2643. }
  2644. }
  2645. break;
  2646. }
  2647. if (added.length) {
  2648. this._trigger('add', {items: added}, senderId);
  2649. }
  2650. if (updated.length) {
  2651. this._trigger('update', {items: updated}, senderId);
  2652. }
  2653. if (removed.length) {
  2654. this._trigger('remove', {items: removed}, senderId);
  2655. }
  2656. }
  2657. };
  2658. // copy subscription functionality from DataSet
  2659. DataView.prototype.on = DataSet.prototype.on;
  2660. DataView.prototype.off = DataSet.prototype.off;
  2661. DataView.prototype._trigger = DataSet.prototype._trigger;
  2662. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2663. DataView.prototype.subscribe = DataView.prototype.on;
  2664. DataView.prototype.unsubscribe = DataView.prototype.off;
  2665. /**
  2666. * @constructor Group
  2667. * @param {Number | String} groupId
  2668. * @param {Object} data
  2669. * @param {ItemSet} itemSet
  2670. */
  2671. function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
  2672. this.id = groupId;
  2673. var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
  2674. this.options = util.selectiveBridgeObject(fields,options);
  2675. this.usingDefaultStyle = group.className === undefined;
  2676. this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
  2677. this.zeroPosition = 0;
  2678. this.update(group);
  2679. if (this.usingDefaultStyle == true) {
  2680. this.groupsUsingDefaultStyles[0] += 1;
  2681. }
  2682. this.itemsData = [];
  2683. }
  2684. GraphGroup.prototype.setItems = function(items) {
  2685. if (items != null) {
  2686. this.itemsData = items;
  2687. if (this.options.sort == true) {
  2688. this.itemsData.sort(function (a,b) {return a.x - b.x;})
  2689. }
  2690. }
  2691. else {
  2692. this.itemsData = [];
  2693. }
  2694. }
  2695. GraphGroup.prototype.setZeroPosition = function(pos) {
  2696. this.zeroPosition = pos;
  2697. }
  2698. GraphGroup.prototype.setOptions = function(options) {
  2699. if (options !== undefined) {
  2700. var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
  2701. util.selectiveDeepExtend(fields, this.options, options);
  2702. util.mergeOptions(this.options, options,'catmullRom');
  2703. util.mergeOptions(this.options, options,'drawPoints');
  2704. util.mergeOptions(this.options, options,'shaded');
  2705. if (options.catmullRom) {
  2706. if (typeof options.catmullRom == 'object') {
  2707. if (options.catmullRom.parametrization) {
  2708. if (options.catmullRom.parametrization == 'uniform') {
  2709. this.options.catmullRom.alpha = 0;
  2710. }
  2711. else if (options.catmullRom.parametrization == 'chordal') {
  2712. this.options.catmullRom.alpha = 1.0;
  2713. }
  2714. else {
  2715. this.options.catmullRom.parametrization = 'centripetal';
  2716. this.options.catmullRom.alpha = 0.5;
  2717. }
  2718. }
  2719. }
  2720. }
  2721. }
  2722. };
  2723. GraphGroup.prototype.update = function(group) {
  2724. this.group = group;
  2725. this.content = group.content || 'graph';
  2726. this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
  2727. this.setOptions(group.options);
  2728. };
  2729. GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
  2730. var fillHeight = iconHeight * 0.5;
  2731. var path, fillPath;
  2732. var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
  2733. outline.setAttributeNS(null, "x", x);
  2734. outline.setAttributeNS(null, "y", y - fillHeight);
  2735. outline.setAttributeNS(null, "width", iconWidth);
  2736. outline.setAttributeNS(null, "height", 2*fillHeight);
  2737. outline.setAttributeNS(null, "class", "outline");
  2738. if (this.options.style == 'line') {
  2739. path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
  2740. path.setAttributeNS(null, "class", this.className);
  2741. path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
  2742. if (this.options.shaded.enabled == true) {
  2743. fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
  2744. if (this.options.shaded.orientation == 'top') {
  2745. fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
  2746. "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
  2747. }
  2748. else {
  2749. fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
  2750. "L"+x+"," + (y + fillHeight) + " " +
  2751. "L"+ (x + iconWidth) + "," + (y + fillHeight) +
  2752. "L"+ (x + iconWidth) + ","+y);
  2753. }
  2754. fillPath.setAttributeNS(null, "class", this.className + " iconFill");
  2755. }
  2756. if (this.options.drawPoints.enabled == true) {
  2757. DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
  2758. }
  2759. }
  2760. else {
  2761. var barWidth = Math.round(0.3 * iconWidth);
  2762. var bar1Height = Math.round(0.4 * iconHeight);
  2763. var bar2Height = Math.round(0.75 * iconHeight);
  2764. var offset = Math.round((iconWidth - (2 * barWidth))/3);
  2765. DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
  2766. DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
  2767. }
  2768. }
  2769. /**
  2770. * Created by Alex on 6/17/14.
  2771. */
  2772. function Legend(body, options, side) {
  2773. this.body = body;
  2774. this.defaultOptions = {
  2775. enabled: true,
  2776. icons: true,
  2777. iconSize: 20,
  2778. iconSpacing: 6,
  2779. left: {
  2780. visible: true,
  2781. position: 'top-left' // top/bottom - left,center,right
  2782. },
  2783. right: {
  2784. visible: true,
  2785. position: 'top-left' // top/bottom - left,center,right
  2786. }
  2787. }
  2788. this.side = side;
  2789. this.options = util.extend({},this.defaultOptions);
  2790. this.svgElements = {};
  2791. this.dom = {};
  2792. this.groups = {};
  2793. this.amountOfGroups = 0;
  2794. this._create();
  2795. this.setOptions(options);
  2796. };
  2797. Legend.prototype = new Component();
  2798. Legend.prototype.addGroup = function(label, graphOptions) {
  2799. if (!this.groups.hasOwnProperty(label)) {
  2800. this.groups[label] = graphOptions;
  2801. }
  2802. this.amountOfGroups += 1;
  2803. };
  2804. Legend.prototype.updateGroup = function(label, graphOptions) {
  2805. this.groups[label] = graphOptions;
  2806. };
  2807. Legend.prototype.removeGroup = function(label) {
  2808. if (this.groups.hasOwnProperty(label)) {
  2809. delete this.groups[label];
  2810. this.amountOfGroups -= 1;
  2811. }
  2812. };
  2813. Legend.prototype._create = function() {
  2814. this.dom.frame = document.createElement('div');
  2815. this.dom.frame.className = 'legend';
  2816. this.dom.frame.style.position = "absolute";
  2817. this.dom.frame.style.top = "10px";
  2818. this.dom.frame.style.display = "block";
  2819. this.dom.textArea = document.createElement('div');
  2820. this.dom.textArea.className = 'legendText';
  2821. this.dom.textArea.style.position = "relative";
  2822. this.dom.textArea.style.top = "0px";
  2823. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  2824. this.svg.style.position = 'absolute';
  2825. this.svg.style.top = 0 +'px';
  2826. this.svg.style.width = this.options.iconSize + 5 + 'px';
  2827. this.dom.frame.appendChild(this.svg);
  2828. this.dom.frame.appendChild(this.dom.textArea);
  2829. }
  2830. /**
  2831. * Hide the component from the DOM
  2832. */
  2833. Legend.prototype.hide = function() {
  2834. // remove the frame containing the items
  2835. if (this.dom.frame.parentNode) {
  2836. this.dom.frame.parentNode.removeChild(this.dom.frame);
  2837. }
  2838. };
  2839. /**
  2840. * Show the component in the DOM (when not already visible).
  2841. * @return {Boolean} changed
  2842. */
  2843. Legend.prototype.show = function() {
  2844. // show frame containing the items
  2845. if (!this.dom.frame.parentNode) {
  2846. this.body.dom.center.appendChild(this.dom.frame);
  2847. }
  2848. };
  2849. Legend.prototype.setOptions = function(options) {
  2850. var fields = ['enabled','orientation','icons','left','right'];
  2851. util.selectiveDeepExtend(fields, this.options, options);
  2852. }
  2853. Legend.prototype.redraw = function() {
  2854. if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false) {
  2855. this.hide();
  2856. }
  2857. else {
  2858. this.show();
  2859. if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
  2860. this.dom.frame.style.left = '4px';
  2861. this.dom.frame.style.textAlign = "left";
  2862. this.dom.textArea.style.textAlign = "left";
  2863. this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
  2864. this.dom.textArea.style.right = '';
  2865. this.svg.style.left = 0 +'px';
  2866. this.svg.style.right = '';
  2867. }
  2868. else {
  2869. this.dom.frame.style.right = '4px';
  2870. this.dom.frame.style.textAlign = "right";
  2871. this.dom.textArea.style.textAlign = "right";
  2872. this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
  2873. this.dom.textArea.style.left = '';
  2874. this.svg.style.right = 0 +'px';
  2875. this.svg.style.left = '';
  2876. }
  2877. if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
  2878. this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
  2879. this.dom.frame.style.bottom = '';
  2880. }
  2881. else {
  2882. this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
  2883. this.dom.frame.style.top = '';
  2884. }
  2885. if (this.options.icons == false) {
  2886. this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
  2887. this.dom.textArea.style.right = '';
  2888. this.dom.textArea.style.left = '';
  2889. this.svg.style.width = '0px';
  2890. }
  2891. else {
  2892. this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
  2893. this.drawLegendIcons();
  2894. }
  2895. var content = "";
  2896. for (var groupId in this.groups) {
  2897. if (this.groups.hasOwnProperty(groupId)) {
  2898. content += this.groups[groupId].content + '<br />';
  2899. }
  2900. }
  2901. this.dom.textArea.innerHTML = content;
  2902. this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
  2903. }
  2904. }
  2905. Legend.prototype.drawLegendIcons = function() {
  2906. if (this.dom.frame.parentNode) {
  2907. DOMutil.prepareElements(this.svgElements);
  2908. var padding = window.getComputedStyle(this.dom.frame).paddingTop;
  2909. var iconOffset = Number(padding.replace("px",''));
  2910. var x = iconOffset;
  2911. var iconWidth = this.options.iconSize;
  2912. var iconHeight = 0.75 * this.options.iconSize;
  2913. var y = iconOffset + 0.5 * iconHeight + 3;
  2914. this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
  2915. for (var groupId in this.groups) {
  2916. if (this.groups.hasOwnProperty(groupId)) {
  2917. this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
  2918. y += iconHeight + this.options.iconSpacing;
  2919. }
  2920. }
  2921. DOMutil.cleanupElements(this.svgElements);
  2922. }
  2923. }
  2924. /**
  2925. * A horizontal time axis
  2926. * @param {Object} [options] See DataAxis.setOptions for the available
  2927. * options.
  2928. * @constructor DataAxis
  2929. * @extends Component
  2930. * @param body
  2931. */
  2932. function DataAxis (body, options, svg) {
  2933. this.id = util.randomUUID();
  2934. this.body = body;
  2935. this.defaultOptions = {
  2936. orientation: 'left', // supported: 'left', 'right'
  2937. showMinorLabels: true,
  2938. showMajorLabels: true,
  2939. icons: true,
  2940. majorLinesOffset: 7,
  2941. minorLinesOffset: 4,
  2942. labelOffsetX: 10,
  2943. labelOffsetY: 2,
  2944. iconWidth: 20,
  2945. width: '40px',
  2946. visible: true
  2947. };
  2948. this.linegraphSVG = svg;
  2949. this.props = {};
  2950. this.DOMelements = { // dynamic elements
  2951. lines: {},
  2952. labels: {}
  2953. };
  2954. this.dom = {};
  2955. this.range = {start:0, end:0};
  2956. this.options = util.extend({}, this.defaultOptions);
  2957. this.conversionFactor = 1;
  2958. this.setOptions(options);
  2959. this.width = Number(('' + this.options.width).replace("px",""));
  2960. this.minWidth = this.width;
  2961. this.height = this.linegraphSVG.offsetHeight;
  2962. this.stepPixels = 25;
  2963. this.stepPixelsForced = 25;
  2964. this.lineOffset = 0;
  2965. this.master = true;
  2966. this.svgElements = {};
  2967. this.groups = {};
  2968. this.amountOfGroups = 0;
  2969. // create the HTML DOM
  2970. this._create();
  2971. }
  2972. DataAxis.prototype = new Component();
  2973. DataAxis.prototype.addGroup = function(label, graphOptions) {
  2974. if (!this.groups.hasOwnProperty(label)) {
  2975. this.groups[label] = graphOptions;
  2976. }
  2977. this.amountOfGroups += 1;
  2978. };
  2979. DataAxis.prototype.updateGroup = function(label, graphOptions) {
  2980. this.groups[label] = graphOptions;
  2981. };
  2982. DataAxis.prototype.removeGroup = function(label) {
  2983. if (this.groups.hasOwnProperty(label)) {
  2984. delete this.groups[label];
  2985. this.amountOfGroups -= 1;
  2986. }
  2987. };
  2988. DataAxis.prototype.setOptions = function (options) {
  2989. if (options) {
  2990. var redraw = false;
  2991. if (this.options.orientation != options.orientation && options.orientation !== undefined) {
  2992. redraw = true;
  2993. }
  2994. var fields = [
  2995. 'orientation',
  2996. 'showMinorLabels',
  2997. 'showMajorLabels',
  2998. 'icons',
  2999. 'majorLinesOffset',
  3000. 'minorLinesOffset',
  3001. 'labelOffsetX',
  3002. 'labelOffsetY',
  3003. 'iconWidth',
  3004. 'width',
  3005. 'visible'];
  3006. util.selectiveExtend(fields, this.options, options);
  3007. this.minWidth = Number(('' + this.options.width).replace("px",""));
  3008. if (redraw == true && this.dom.frame) {
  3009. this.hide();
  3010. this.show();
  3011. }
  3012. }
  3013. };
  3014. /**
  3015. * Create the HTML DOM for the DataAxis
  3016. */
  3017. DataAxis.prototype._create = function() {
  3018. this.dom.frame = document.createElement('div');
  3019. this.dom.frame.style.width = this.options.width;
  3020. this.dom.frame.style.height = this.height;
  3021. this.dom.lineContainer = document.createElement('div');
  3022. this.dom.lineContainer.style.width = '100%';
  3023. this.dom.lineContainer.style.height = this.height;
  3024. // create svg element for graph drawing.
  3025. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  3026. this.svg.style.position = "absolute";
  3027. this.svg.style.top = '0px';
  3028. this.svg.style.height = '100%';
  3029. this.svg.style.width = '100%';
  3030. this.svg.style.display = "block";
  3031. this.dom.frame.appendChild(this.svg);
  3032. };
  3033. DataAxis.prototype._redrawGroupIcons = function () {
  3034. DOMutil.prepareElements(this.svgElements);
  3035. var x;
  3036. var iconWidth = this.options.iconWidth;
  3037. var iconHeight = 15;
  3038. var iconOffset = 4;
  3039. var y = iconOffset + 0.5 * iconHeight;
  3040. if (this.options.orientation == 'left') {
  3041. x = iconOffset;
  3042. }
  3043. else {
  3044. x = this.width - iconWidth - iconOffset;
  3045. }
  3046. for (var groupId in this.groups) {
  3047. if (this.groups.hasOwnProperty(groupId)) {
  3048. this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
  3049. y += iconHeight + iconOffset;
  3050. }
  3051. }
  3052. DOMutil.cleanupElements(this.svgElements);
  3053. };
  3054. /**
  3055. * Create the HTML DOM for the DataAxis
  3056. */
  3057. DataAxis.prototype.show = function() {
  3058. if (!this.dom.frame.parentNode) {
  3059. if (this.options.orientation == 'left') {
  3060. this.body.dom.left.appendChild(this.dom.frame);
  3061. }
  3062. else {
  3063. this.body.dom.right.appendChild(this.dom.frame);
  3064. }
  3065. }
  3066. if (!this.dom.lineContainer.parentNode) {
  3067. this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
  3068. }
  3069. };
  3070. /**
  3071. * Create the HTML DOM for the DataAxis
  3072. */
  3073. DataAxis.prototype.hide = function() {
  3074. if (this.dom.frame.parentNode) {
  3075. this.dom.frame.parentNode.removeChild(this.dom.frame);
  3076. }
  3077. if (this.dom.lineContainer.parentNode) {
  3078. this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
  3079. }
  3080. };
  3081. /**
  3082. * Set a range (start and end)
  3083. * @param end
  3084. * @param start
  3085. * @param end
  3086. */
  3087. DataAxis.prototype.setRange = function (start, end) {
  3088. this.range.start = start;
  3089. this.range.end = end;
  3090. };
  3091. /**
  3092. * Repaint the component
  3093. * @return {boolean} Returns true if the component is resized
  3094. */
  3095. DataAxis.prototype.redraw = function () {
  3096. var changeCalled = false;
  3097. if (this.amountOfGroups == 0) {
  3098. this.hide();
  3099. }
  3100. else {
  3101. this.show();
  3102. this.height = Number(this.linegraphSVG.style.height.replace("px",""));
  3103. // svg offsetheight did not work in firefox and explorer...
  3104. this.dom.lineContainer.style.height = this.height + 'px';
  3105. this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
  3106. var props = this.props;
  3107. var frame = this.dom.frame;
  3108. // update classname
  3109. frame.className = 'dataaxis';
  3110. // calculate character width and height
  3111. this._calculateCharSize();
  3112. var orientation = this.options.orientation;
  3113. var showMinorLabels = this.options.showMinorLabels;
  3114. var showMajorLabels = this.options.showMajorLabels;
  3115. // determine the width and height of the elemens for the axis
  3116. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3117. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3118. props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
  3119. props.minorLineHeight = 1;
  3120. props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
  3121. props.majorLineHeight = 1;
  3122. // take frame offline while updating (is almost twice as fast)
  3123. if (orientation == 'left') {
  3124. frame.style.top = '0';
  3125. frame.style.left = '0';
  3126. frame.style.bottom = '';
  3127. frame.style.width = this.width + 'px';
  3128. frame.style.height = this.height + "px";
  3129. }
  3130. else { // right
  3131. frame.style.top = '';
  3132. frame.style.bottom = '0';
  3133. frame.style.left = '0';
  3134. frame.style.width = this.width + 'px';
  3135. frame.style.height = this.height + "px";
  3136. }
  3137. changeCalled = this._redrawLabels();
  3138. if (this.options.icons == true) {
  3139. this._redrawGroupIcons();
  3140. }
  3141. }
  3142. return changeCalled;
  3143. };
  3144. /**
  3145. * Repaint major and minor text labels and vertical grid lines
  3146. * @private
  3147. */
  3148. DataAxis.prototype._redrawLabels = function () {
  3149. DOMutil.prepareElements(this.DOMelements);
  3150. var orientation = this.options['orientation'];
  3151. // calculate range and step (step such that we have space for 7 characters per label)
  3152. var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
  3153. var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight);
  3154. this.step = step;
  3155. step.first();
  3156. // get the distance in pixels for a step
  3157. var stepPixels = this.dom.frame.offsetHeight / ((step.marginRange / step.step) + 1);
  3158. this.stepPixels = stepPixels;
  3159. var amountOfSteps = this.height / stepPixels;
  3160. var stepDifference = 0;
  3161. if (this.master == false) {
  3162. stepPixels = this.stepPixelsForced;
  3163. stepDifference = Math.round((this.height / stepPixels) - amountOfSteps);
  3164. for (var i = 0; i < 0.5 * stepDifference; i++) {
  3165. step.previous();
  3166. }
  3167. amountOfSteps = this.height / stepPixels;
  3168. }
  3169. this.valueAtZero = step.marginEnd;
  3170. var marginStartPos = 0;
  3171. // do not draw the first label
  3172. var max = 1;
  3173. step.next();
  3174. this.maxLabelSize = 0;
  3175. var y = 0;
  3176. while (max < Math.round(amountOfSteps)) {
  3177. y = Math.round(max * stepPixels);
  3178. marginStartPos = max * stepPixels;
  3179. var isMajor = step.isMajor();
  3180. if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
  3181. this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
  3182. }
  3183. if (isMajor && this.options['showMajorLabels'] && this.master == true ||
  3184. this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
  3185. if (y >= 0) {
  3186. this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
  3187. }
  3188. this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
  3189. }
  3190. else {
  3191. this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
  3192. }
  3193. step.next();
  3194. max++;
  3195. }
  3196. this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step);
  3197. var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
  3198. // this will resize the yAxis to accomodate the labels.
  3199. if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
  3200. this.width = this.maxLabelSize + offset;
  3201. this.options.width = this.width + "px";
  3202. DOMutil.cleanupElements(this.DOMelements);
  3203. this.redraw();
  3204. return true;
  3205. }
  3206. // this will resize the yAxis if it is too big for the labels.
  3207. else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
  3208. this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
  3209. this.options.width = this.width + "px";
  3210. DOMutil.cleanupElements(this.DOMelements);
  3211. this.redraw();
  3212. return true;
  3213. }
  3214. else {
  3215. DOMutil.cleanupElements(this.DOMelements);
  3216. return false;
  3217. }
  3218. };
  3219. /**
  3220. * Create a label for the axis at position x
  3221. * @private
  3222. * @param y
  3223. * @param text
  3224. * @param orientation
  3225. * @param className
  3226. * @param characterHeight
  3227. */
  3228. DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
  3229. // reuse redundant label
  3230. var label = DOMutil.getDOMElement('div',this.DOMelements, this.dom.frame); //this.dom.redundant.labels.shift();
  3231. label.className = className;
  3232. label.innerHTML = text;
  3233. if (orientation == 'left') {
  3234. label.style.left = '-' + this.options.labelOffsetX + 'px';
  3235. label.style.textAlign = "right";
  3236. }
  3237. else {
  3238. label.style.right = '-' + this.options.labelOffsetX + 'px';
  3239. label.style.textAlign = "left";
  3240. }
  3241. label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
  3242. text += '';
  3243. var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
  3244. if (this.maxLabelSize < text.length * largestWidth) {
  3245. this.maxLabelSize = text.length * largestWidth;
  3246. }
  3247. };
  3248. /**
  3249. * Create a minor line for the axis at position y
  3250. * @param y
  3251. * @param orientation
  3252. * @param className
  3253. * @param offset
  3254. * @param width
  3255. */
  3256. DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
  3257. if (this.master == true) {
  3258. var line = DOMutil.getDOMElement('div',this.DOMelements, this.dom.lineContainer);//this.dom.redundant.lines.shift();
  3259. line.className = className;
  3260. line.innerHTML = '';
  3261. if (orientation == 'left') {
  3262. line.style.left = (this.width - offset) + 'px';
  3263. }
  3264. else {
  3265. line.style.right = (this.width - offset) + 'px';
  3266. }
  3267. line.style.width = width + 'px';
  3268. line.style.top = y + 'px';
  3269. }
  3270. };
  3271. DataAxis.prototype.convertValue = function (value) {
  3272. var invertedValue = this.valueAtZero - value;
  3273. var convertedValue = invertedValue * this.conversionFactor;
  3274. return convertedValue; // the -2 is to compensate for the borders
  3275. };
  3276. /**
  3277. * Determine the size of text on the axis (both major and minor axis).
  3278. * The size is calculated only once and then cached in this.props.
  3279. * @private
  3280. */
  3281. DataAxis.prototype._calculateCharSize = function () {
  3282. // determine the char width and height on the minor axis
  3283. if (!('minorCharHeight' in this.props)) {
  3284. var textMinor = document.createTextNode('0');
  3285. var measureCharMinor = document.createElement('DIV');
  3286. measureCharMinor.className = 'yAxis minor measure';
  3287. measureCharMinor.appendChild(textMinor);
  3288. this.dom.frame.appendChild(measureCharMinor);
  3289. this.props.minorCharHeight = measureCharMinor.clientHeight;
  3290. this.props.minorCharWidth = measureCharMinor.clientWidth;
  3291. this.dom.frame.removeChild(measureCharMinor);
  3292. }
  3293. if (!('majorCharHeight' in this.props)) {
  3294. var textMajor = document.createTextNode('0');
  3295. var measureCharMajor = document.createElement('DIV');
  3296. measureCharMajor.className = 'yAxis major measure';
  3297. measureCharMajor.appendChild(textMajor);
  3298. this.dom.frame.appendChild(measureCharMajor);
  3299. this.props.majorCharHeight = measureCharMajor.clientHeight;
  3300. this.props.majorCharWidth = measureCharMajor.clientWidth;
  3301. this.dom.frame.removeChild(measureCharMajor);
  3302. }
  3303. };
  3304. /**
  3305. * Snap a date to a rounded value.
  3306. * The snap intervals are dependent on the current scale and step.
  3307. * @param {Date} date the date to be snapped.
  3308. * @return {Date} snappedDate
  3309. */
  3310. DataAxis.prototype.snap = function(date) {
  3311. return this.step.snap(date);
  3312. };
  3313. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  3314. /**
  3315. * This is the constructor of the LineGraph. It requires a Timeline body and options.
  3316. *
  3317. * @param body
  3318. * @param options
  3319. * @constructor
  3320. */
  3321. function LineGraph(body, options) {
  3322. this.id = util.randomUUID();
  3323. this.body = body;
  3324. this.defaultOptions = {
  3325. yAxisOrientation: 'left',
  3326. defaultGroup: 'default',
  3327. sort: true,
  3328. sampling: true,
  3329. graphHeight: '400px',
  3330. shaded: {
  3331. enabled: false,
  3332. orientation: 'bottom' // top, bottom
  3333. },
  3334. style: 'line', // line, bar
  3335. barChart: {
  3336. width: 50,
  3337. align: 'center' // left, center, right
  3338. },
  3339. catmullRom: {
  3340. enabled: true,
  3341. parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
  3342. alpha: 0.5
  3343. },
  3344. drawPoints: {
  3345. enabled: true,
  3346. size: 6,
  3347. style: 'square' // square, circle
  3348. },
  3349. dataAxis: {
  3350. showMinorLabels: true,
  3351. showMajorLabels: true,
  3352. icons: false,
  3353. width: '40px',
  3354. visible: true
  3355. },
  3356. legend: {
  3357. enabled: false,
  3358. icons: true,
  3359. left: {
  3360. visible: true,
  3361. position: 'top-left' // top/bottom - left,right
  3362. },
  3363. right: {
  3364. visible: true,
  3365. position: 'top-right' // top/bottom - left,right
  3366. }
  3367. }
  3368. };
  3369. // options is shared by this ItemSet and all its items
  3370. this.options = util.extend({}, this.defaultOptions);
  3371. this.dom = {};
  3372. this.props = {};
  3373. this.hammer = null;
  3374. this.groups = {};
  3375. var me = this;
  3376. this.itemsData = null; // DataSet
  3377. this.groupsData = null; // DataSet
  3378. // listeners for the DataSet of the items
  3379. this.itemListeners = {
  3380. 'add': function (event, params, senderId) {
  3381. me._onAdd(params.items);
  3382. },
  3383. 'update': function (event, params, senderId) {
  3384. me._onUpdate(params.items);
  3385. },
  3386. 'remove': function (event, params, senderId) {
  3387. me._onRemove(params.items);
  3388. }
  3389. };
  3390. // listeners for the DataSet of the groups
  3391. this.groupListeners = {
  3392. 'add': function (event, params, senderId) {
  3393. me._onAddGroups(params.items);
  3394. },
  3395. 'update': function (event, params, senderId) {
  3396. me._onUpdateGroups(params.items);
  3397. },
  3398. 'remove': function (event, params, senderId) {
  3399. me._onRemoveGroups(params.items);
  3400. }
  3401. };
  3402. this.items = {}; // object with an Item for every data item
  3403. this.selection = []; // list with the ids of all selected nodes
  3404. this.lastStart = this.body.range.start;
  3405. this.touchParams = {}; // stores properties while dragging
  3406. this.svgElements = {};
  3407. this.setOptions(options);
  3408. this.groupsUsingDefaultStyles = [0];
  3409. this.body.emitter.on("rangechange",function() {
  3410. if (me.lastStart != 0) {
  3411. var offset = me.body.range.start - me.lastStart;
  3412. var range = me.body.range.end - me.body.range.start;
  3413. if (me.width != 0) {
  3414. var rangePerPixelInv = me.width/range;
  3415. var xOffset = offset * rangePerPixelInv;
  3416. me.svg.style.left = (-me.width - xOffset) + "px";
  3417. }
  3418. }
  3419. });
  3420. this.body.emitter.on("rangechanged", function() {
  3421. me.lastStart = me.body.range.start;
  3422. me.svg.style.left = util.option.asSize(-me.width);
  3423. me._updateGraph.apply(me);
  3424. });
  3425. // create the HTML DOM
  3426. this._create();
  3427. this.body.emitter.emit("change");
  3428. }
  3429. LineGraph.prototype = new Component();
  3430. /**
  3431. * Create the HTML DOM for the ItemSet
  3432. */
  3433. LineGraph.prototype._create = function(){
  3434. var frame = document.createElement('div');
  3435. frame.className = 'LineGraph';
  3436. this.dom.frame = frame;
  3437. // create svg element for graph drawing.
  3438. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  3439. this.svg.style.position = "relative";
  3440. this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
  3441. this.svg.style.display = "block";
  3442. frame.appendChild(this.svg);
  3443. // data axis
  3444. this.options.dataAxis.orientation = 'left';
  3445. this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg);
  3446. this.options.dataAxis.orientation = 'right';
  3447. this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg);
  3448. delete this.options.dataAxis.orientation;
  3449. // legends
  3450. this.legendLeft = new Legend(this.body, this.options.legend, 'left');
  3451. this.legendRight = new Legend(this.body, this.options.legend, 'right');
  3452. this.show();
  3453. };
  3454. /**
  3455. * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
  3456. * @param options
  3457. */
  3458. LineGraph.prototype.setOptions = function(options) {
  3459. if (options) {
  3460. var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort'];
  3461. util.selectiveDeepExtend(fields, this.options, options);
  3462. util.mergeOptions(this.options, options,'catmullRom');
  3463. util.mergeOptions(this.options, options,'drawPoints');
  3464. util.mergeOptions(this.options, options,'shaded');
  3465. util.mergeOptions(this.options, options,'legend');
  3466. if (options.catmullRom) {
  3467. if (typeof options.catmullRom == 'object') {
  3468. if (options.catmullRom.parametrization) {
  3469. if (options.catmullRom.parametrization == 'uniform') {
  3470. this.options.catmullRom.alpha = 0;
  3471. }
  3472. else if (options.catmullRom.parametrization == 'chordal') {
  3473. this.options.catmullRom.alpha = 1.0;
  3474. }
  3475. else {
  3476. this.options.catmullRom.parametrization = 'centripetal';
  3477. this.options.catmullRom.alpha = 0.5;
  3478. }
  3479. }
  3480. }
  3481. }
  3482. if (this.yAxisLeft) {
  3483. if (options.dataAxis !== undefined) {
  3484. this.yAxisLeft.setOptions(this.options.dataAxis);
  3485. this.yAxisRight.setOptions(this.options.dataAxis);
  3486. }
  3487. }
  3488. if (this.legendLeft) {
  3489. if (options.legend !== undefined) {
  3490. this.legendLeft.setOptions(this.options.legend);
  3491. this.legendRight.setOptions(this.options.legend);
  3492. }
  3493. }
  3494. if (this.groups.hasOwnProperty(UNGROUPED)) {
  3495. this.groups[UNGROUPED].setOptions(options);
  3496. }
  3497. }
  3498. if (this.dom.frame) {
  3499. this._updateGraph();
  3500. }
  3501. };
  3502. /**
  3503. * Hide the component from the DOM
  3504. */
  3505. LineGraph.prototype.hide = function() {
  3506. // remove the frame containing the items
  3507. if (this.dom.frame.parentNode) {
  3508. this.dom.frame.parentNode.removeChild(this.dom.frame);
  3509. }
  3510. };
  3511. /**
  3512. * Show the component in the DOM (when not already visible).
  3513. * @return {Boolean} changed
  3514. */
  3515. LineGraph.prototype.show = function() {
  3516. // show frame containing the items
  3517. if (!this.dom.frame.parentNode) {
  3518. this.body.dom.center.appendChild(this.dom.frame);
  3519. }
  3520. };
  3521. /**
  3522. * Set items
  3523. * @param {vis.DataSet | null} items
  3524. */
  3525. LineGraph.prototype.setItems = function(items) {
  3526. var me = this,
  3527. ids,
  3528. oldItemsData = this.itemsData;
  3529. // replace the dataset
  3530. if (!items) {
  3531. this.itemsData = null;
  3532. }
  3533. else if (items instanceof DataSet || items instanceof DataView) {
  3534. this.itemsData = items;
  3535. }
  3536. else {
  3537. throw new TypeError('Data must be an instance of DataSet or DataView');
  3538. }
  3539. if (oldItemsData) {
  3540. // unsubscribe from old dataset
  3541. util.forEach(this.itemListeners, function (callback, event) {
  3542. oldItemsData.off(event, callback);
  3543. });
  3544. // remove all drawn items
  3545. ids = oldItemsData.getIds();
  3546. this._onRemove(ids);
  3547. }
  3548. if (this.itemsData) {
  3549. // subscribe to new dataset
  3550. var id = this.id;
  3551. util.forEach(this.itemListeners, function (callback, event) {
  3552. me.itemsData.on(event, callback, id);
  3553. });
  3554. // add all new items
  3555. ids = this.itemsData.getIds();
  3556. this._onAdd(ids);
  3557. }
  3558. this._updateUngrouped();
  3559. this._updateGraph();
  3560. this.redraw();
  3561. };
  3562. /**
  3563. * Set groups
  3564. * @param {vis.DataSet} groups
  3565. */
  3566. LineGraph.prototype.setGroups = function(groups) {
  3567. var me = this,
  3568. ids;
  3569. // unsubscribe from current dataset
  3570. if (this.groupsData) {
  3571. util.forEach(this.groupListeners, function (callback, event) {
  3572. me.groupsData.unsubscribe(event, callback);
  3573. });
  3574. // remove all drawn groups
  3575. ids = this.groupsData.getIds();
  3576. this.groupsData = null;
  3577. this._onRemoveGroups(ids); // note: this will cause a redraw
  3578. }
  3579. // replace the dataset
  3580. if (!groups) {
  3581. this.groupsData = null;
  3582. }
  3583. else if (groups instanceof DataSet || groups instanceof DataView) {
  3584. this.groupsData = groups;
  3585. }
  3586. else {
  3587. throw new TypeError('Data must be an instance of DataSet or DataView');
  3588. }
  3589. if (this.groupsData) {
  3590. // subscribe to new dataset
  3591. var id = this.id;
  3592. util.forEach(this.groupListeners, function (callback, event) {
  3593. me.groupsData.on(event, callback, id);
  3594. });
  3595. // draw all ms
  3596. ids = this.groupsData.getIds();
  3597. this._onAddGroups(ids);
  3598. }
  3599. this._onUpdate();
  3600. };
  3601. LineGraph.prototype._onUpdate = function(ids) {
  3602. this._updateUngrouped();
  3603. this._updateAllGroupData();
  3604. this._updateGraph();
  3605. this.redraw();
  3606. };
  3607. LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
  3608. LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
  3609. LineGraph.prototype._onUpdateGroups = function (groupIds) {
  3610. for (var i = 0; i < groupIds.length; i++) {
  3611. var group = this.groupsData.get(groupIds[i]);
  3612. this._updateGroup(group, groupIds[i]);
  3613. }
  3614. this._updateGraph();
  3615. this.redraw();
  3616. };
  3617. LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
  3618. LineGraph.prototype._onRemoveGroups = function (groupIds) {
  3619. for (var i = 0; i < groupIds.length; i++) {
  3620. if (!this.groups.hasOwnProperty(groupIds[i])) {
  3621. if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
  3622. this.yAxisRight.removeGroup(groupIds[i]);
  3623. this.legendRight.removeGroup(groupIds[i]);
  3624. this.legendRight.redraw();
  3625. }
  3626. else {
  3627. this.yAxisLeft.removeGroup(groupIds[i]);
  3628. this.legendLeft.removeGroup(groupIds[i]);
  3629. this.legendLeft.redraw();
  3630. }
  3631. delete this.groups[groupIds[i]];
  3632. }
  3633. }
  3634. this._updateUngrouped();
  3635. this._updateGraph();
  3636. this.redraw();
  3637. };
  3638. /**
  3639. * update a group object
  3640. *
  3641. * @param group
  3642. * @param groupId
  3643. * @private
  3644. */
  3645. LineGraph.prototype._updateGroup = function (group, groupId) {
  3646. if (!this.groups.hasOwnProperty(groupId)) {
  3647. this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
  3648. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  3649. this.yAxisRight.addGroup(groupId, this.groups[groupId]);
  3650. this.legendRight.addGroup(groupId, this.groups[groupId]);
  3651. }
  3652. else {
  3653. this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
  3654. this.legendLeft.addGroup(groupId, this.groups[groupId]);
  3655. }
  3656. }
  3657. else {
  3658. this.groups[groupId].update(group);
  3659. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  3660. this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
  3661. this.legendRight.updateGroup(groupId, this.groups[groupId]);
  3662. }
  3663. else {
  3664. this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
  3665. this.legendLeft.updateGroup(groupId, this.groups[groupId]);
  3666. }
  3667. }
  3668. this.legendLeft.redraw();
  3669. this.legendRight.redraw();
  3670. };
  3671. LineGraph.prototype._updateAllGroupData = function () {
  3672. if (this.itemsData != null) {
  3673. // ~450 ms @ 500k
  3674. var groupsContent = {};
  3675. for (var groupId in this.groups) {
  3676. if (this.groups.hasOwnProperty(groupId)) {
  3677. groupsContent[groupId] = [];
  3678. }
  3679. }
  3680. for (var itemId in this.itemsData._data) {
  3681. if (this.itemsData._data.hasOwnProperty(itemId)) {
  3682. var item = this.itemsData._data[itemId];
  3683. item.x = util.convert(item.x,"Date");
  3684. groupsContent[item.group].push(item);
  3685. }
  3686. }
  3687. for (var groupId in this.groups) {
  3688. if (this.groups.hasOwnProperty(groupId)) {
  3689. this.groups[groupId].setItems(groupsContent[groupId]);
  3690. }
  3691. }
  3692. // // ~4500ms @ 500k
  3693. // for (var groupId in this.groups) {
  3694. // if (this.groups.hasOwnProperty(groupId)) {
  3695. // this.groups[groupId].setItems(this.itemsData.get({filter:
  3696. // function (item) {
  3697. // return (item.group == groupId);
  3698. // }, type:{x:"Date"}}
  3699. // ));
  3700. // }
  3701. // }
  3702. }
  3703. };
  3704. /**
  3705. * Create or delete the group holding all ungrouped items. This group is used when
  3706. * there are no groups specified. This anonymous group is called 'graph'.
  3707. * @protected
  3708. */
  3709. LineGraph.prototype._updateUngrouped = function() {
  3710. if (this.itemsData != null) {
  3711. // var t0 = new Date();
  3712. var group = {id: UNGROUPED, content: this.options.defaultGroup};
  3713. this._updateGroup(group, UNGROUPED);
  3714. var ungroupedCounter = 0;
  3715. if (this.itemsData) {
  3716. for (var itemId in this.itemsData._data) {
  3717. if (this.itemsData._data.hasOwnProperty(itemId)) {
  3718. var item = this.itemsData._data[itemId];
  3719. if (item != undefined) {
  3720. if (item.hasOwnProperty('group')) {
  3721. if (item.group === undefined) {
  3722. item.group = UNGROUPED;
  3723. }
  3724. }
  3725. else {
  3726. item.group = UNGROUPED;
  3727. }
  3728. ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
  3729. }
  3730. }
  3731. }
  3732. }
  3733. // much much slower
  3734. // var datapoints = this.itemsData.get({
  3735. // filter: function (item) {return item.group === undefined;},
  3736. // showInternalIds:true
  3737. // });
  3738. // if (datapoints.length > 0) {
  3739. // var updateQuery = [];
  3740. // for (var i = 0; i < datapoints.length; i++) {
  3741. // updateQuery.push({id:datapoints[i].id, group: UNGROUPED});
  3742. // }
  3743. // this.itemsData.update(updateQuery, true);
  3744. // }
  3745. // var t1 = new Date();
  3746. // var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}});
  3747. if (ungroupedCounter == 0) {
  3748. delete this.groups[UNGROUPED];
  3749. this.legendLeft.removeGroup(UNGROUPED);
  3750. this.legendRight.removeGroup(UNGROUPED);
  3751. this.yAxisLeft.removeGroup(UNGROUPED);
  3752. this.yAxisRight.removeGroup(UNGROUPED);
  3753. }
  3754. // console.log("getting amount ungrouped",new Date() - t1);
  3755. // console.log("putting in ungrouped",new Date() - t0);
  3756. }
  3757. else {
  3758. delete this.groups[UNGROUPED];
  3759. this.legendLeft.removeGroup(UNGROUPED);
  3760. this.legendRight.removeGroup(UNGROUPED);
  3761. this.yAxisLeft.removeGroup(UNGROUPED);
  3762. this.yAxisRight.removeGroup(UNGROUPED);
  3763. }
  3764. this.legendLeft.redraw();
  3765. this.legendRight.redraw();
  3766. };
  3767. /**
  3768. * Redraw the component, mandatory function
  3769. * @return {boolean} Returns true if the component is resized
  3770. */
  3771. LineGraph.prototype.redraw = function() {
  3772. var resized = false;
  3773. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  3774. if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
  3775. resized = true;
  3776. }
  3777. // check if this component is resized
  3778. resized = this._isResized() || resized;
  3779. // check whether zoomed (in that case we need to re-stack everything)
  3780. var visibleInterval = this.body.range.end - this.body.range.start;
  3781. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  3782. this.lastVisibleInterval = visibleInterval;
  3783. this.lastWidth = this.width;
  3784. // calculate actual size and position
  3785. this.width = this.dom.frame.offsetWidth;
  3786. // the svg element is three times as big as the width, this allows for fully dragging left and right
  3787. // without reloading the graph. the controls for this are bound to events in the constructor
  3788. if (resized == true) {
  3789. this.svg.style.width = util.option.asSize(3*this.width);
  3790. this.svg.style.left = util.option.asSize(-this.width);
  3791. }
  3792. if (zoomed == true) {
  3793. this._updateGraph();
  3794. }
  3795. this.legendLeft.redraw();
  3796. this.legendRight.redraw();
  3797. return resized;
  3798. };
  3799. /**
  3800. * Update and redraw the graph.
  3801. *
  3802. */
  3803. LineGraph.prototype._updateGraph = function () {
  3804. // reset the svg elements
  3805. DOMutil.prepareElements(this.svgElements);
  3806. // // very slow...
  3807. // groupData = group.itemsData.get({filter:
  3808. // function (item) {
  3809. // return (item.x > minDate && item.x < maxDate);
  3810. // }}
  3811. // );
  3812. if (this.width != 0 && this.itemsData != null) {
  3813. var group, groupData, preprocessedGroup, i;
  3814. var preprocessedGroupData = [];
  3815. var processedGroupData = [];
  3816. var groupRanges = [];
  3817. var changeCalled = false;
  3818. // getting group Ids
  3819. var groupIds = [];
  3820. for (var groupId in this.groups) {
  3821. if (this.groups.hasOwnProperty(groupId)) {
  3822. groupIds.push(groupId);
  3823. }
  3824. }
  3825. // this is the range of the SVG canvas
  3826. var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
  3827. var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
  3828. // first select and preprocess the data from the datasets.
  3829. // the groups have their preselection of data, we now loop over this data to see
  3830. // what data we need to draw. Sorted data is much faster.
  3831. // more optimization is possible by doing the sampling before and using the binary search
  3832. // to find the end date to determine the increment.
  3833. if (groupIds.length > 0) {
  3834. for (i = 0; i < groupIds.length; i++) {
  3835. group = this.groups[groupIds[i]];
  3836. groupData = [];
  3837. // optimization for sorted data
  3838. if (group.options.sort == true) {
  3839. var guess = Math.max(0,util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
  3840. for (var j = guess; j < group.itemsData.length; j++) {
  3841. var item = group.itemsData[j];
  3842. if (item !== undefined) {
  3843. if (item.x > maxDate) {
  3844. groupData.push(item);
  3845. break;
  3846. }
  3847. else {
  3848. groupData.push(item);
  3849. }
  3850. }
  3851. }
  3852. }
  3853. else {
  3854. for (var j = 0; j < group.itemsData.length; j++) {
  3855. var item = group.itemsData[j];
  3856. if (item !== undefined) {
  3857. if (item.x > minDate && item.x < maxDate) {
  3858. groupData.push(item);
  3859. }
  3860. }
  3861. }
  3862. }
  3863. // preprocess, split into ranges and data
  3864. preprocessedGroup = this._preprocessData(groupData, group);
  3865. groupRanges.push({min: preprocessedGroup.min, max: preprocessedGroup.max});
  3866. preprocessedGroupData.push(preprocessedGroup.data);
  3867. }
  3868. // update the Y axis first, we use this data to draw at the correct Y points
  3869. // changeCalled is required to clean the SVG on a change emit.
  3870. changeCalled = this._updateYAxis(groupIds, groupRanges);
  3871. if (changeCalled == true) {
  3872. DOMutil.cleanupElements(this.svgElements);
  3873. this.body.emitter.emit("change");
  3874. return;
  3875. }
  3876. // with the yAxis scaled correctly, use this to get the Y values of the points.
  3877. for (i = 0; i < groupIds.length; i++) {
  3878. group = this.groups[groupIds[i]];
  3879. processedGroupData.push(this._convertYvalues(preprocessedGroupData[i],group))
  3880. }
  3881. // draw the groups
  3882. for (i = 0; i < groupIds.length; i++) {
  3883. group = this.groups[groupIds[i]];
  3884. if (group.options.style == 'line') {
  3885. this._drawLineGraph(processedGroupData[i], group);
  3886. }
  3887. else {
  3888. this._drawBarGraph (processedGroupData[i], group);
  3889. }
  3890. }
  3891. }
  3892. }
  3893. // cleanup unused svg elements
  3894. DOMutil.cleanupElements(this.svgElements);
  3895. };
  3896. /**
  3897. * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
  3898. * @param {array} groupIds
  3899. * @private
  3900. */
  3901. LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
  3902. var changeCalled = false;
  3903. var yAxisLeftUsed = false;
  3904. var yAxisRightUsed = false;
  3905. var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
  3906. var orientation = 'left';
  3907. // if groups are present
  3908. if (groupIds.length > 0) {
  3909. for (var i = 0; i < groupIds.length; i++) {
  3910. orientation = 'left';
  3911. var group = this.groups[groupIds[i]];
  3912. if (group.options.yAxisOrientation == 'right') {
  3913. orientation = 'right';
  3914. }
  3915. minVal = groupRanges[i].min;
  3916. maxVal = groupRanges[i].max;
  3917. if (orientation == 'left') {
  3918. yAxisLeftUsed = true;
  3919. minLeft = minLeft > minVal ? minVal : minLeft;
  3920. maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
  3921. }
  3922. else {
  3923. yAxisRightUsed = true;
  3924. minRight = minRight > minVal ? minVal : minRight;
  3925. maxRight = maxRight < maxVal ? maxVal : maxRight;
  3926. }
  3927. }
  3928. if (yAxisLeftUsed == true) {
  3929. this.yAxisLeft.setRange(minLeft, maxLeft);
  3930. }
  3931. if (yAxisRightUsed == true) {
  3932. this.yAxisRight.setRange(minRight, maxRight);
  3933. }
  3934. }
  3935. changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
  3936. changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
  3937. if (yAxisRightUsed == true && yAxisLeftUsed == true) {
  3938. this.yAxisLeft.drawIcons = true;
  3939. this.yAxisRight.drawIcons = true;
  3940. }
  3941. else {
  3942. this.yAxisLeft.drawIcons = false;
  3943. this.yAxisRight.drawIcons = false;
  3944. }
  3945. this.yAxisRight.master = !yAxisLeftUsed;
  3946. if (this.yAxisRight.master == false) {
  3947. if (yAxisRightUsed == true) {
  3948. this.yAxisLeft.lineOffset = this.yAxisRight.width;
  3949. }
  3950. changeCalled = this.yAxisLeft.redraw() || changeCalled;
  3951. this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
  3952. changeCalled = this.yAxisRight.redraw() || changeCalled;
  3953. }
  3954. else {
  3955. changeCalled = this.yAxisRight.redraw() || changeCalled;
  3956. }
  3957. return changeCalled;
  3958. };
  3959. /**
  3960. * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
  3961. *
  3962. * @param {boolean} axisUsed
  3963. * @returns {boolean}
  3964. * @private
  3965. * @param axis
  3966. */
  3967. LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
  3968. var changed = false;
  3969. if (axisUsed == false) {
  3970. if (axis.dom.frame.parentNode) {
  3971. axis.hide();
  3972. changed = true;
  3973. }
  3974. }
  3975. else {
  3976. if (!axis.dom.frame.parentNode) {
  3977. axis.show();
  3978. changed = true;
  3979. }
  3980. }
  3981. return changed;
  3982. };
  3983. /**
  3984. * draw a bar graph
  3985. * @param datapoints
  3986. * @param group
  3987. */
  3988. LineGraph.prototype._drawBarGraph = function (dataset, group) {
  3989. if (dataset != null) {
  3990. if (dataset.length > 0) {
  3991. var coreDistance;
  3992. var minWidth = 0.1 * group.options.barChart.width;
  3993. var offset = 0;
  3994. var width = group.options.barChart.width;
  3995. if (group.options.barChart.align == 'left') {offset -= 0.5*width;}
  3996. else if (group.options.barChart.align == 'right') {offset += 0.5*width;}
  3997. for (var i = 0; i < dataset.length; i++) {
  3998. // dynammically downscale the width so there is no overlap up to 1/10th the original width
  3999. if (i+1 < dataset.length) {coreDistance = Math.abs(dataset[i+1].x - dataset[i].x);}
  4000. if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(dataset[i-1].x - dataset[i].x));}
  4001. if (coreDistance < width) {width = coreDistance < minWidth ? minWidth : coreDistance;}
  4002. DOMutil.drawBar(dataset[i].x + offset, dataset[i].y, width, group.zeroPosition - dataset[i].y, group.className + ' bar', this.svgElements, this.svg);
  4003. }
  4004. // draw points
  4005. if (group.options.drawPoints.enabled == true) {
  4006. this._drawPoints(dataset, group, this.svgElements, this.svg, offset);
  4007. }
  4008. }
  4009. }
  4010. };
  4011. /**
  4012. * draw a line graph
  4013. *
  4014. * @param datapoints
  4015. * @param group
  4016. */
  4017. LineGraph.prototype._drawLineGraph = function (dataset, group) {
  4018. if (dataset != null) {
  4019. if (dataset.length > 0) {
  4020. var path, d;
  4021. var svgHeight = Number(this.svg.style.height.replace("px",""));
  4022. path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
  4023. path.setAttributeNS(null, "class", group.className);
  4024. // construct path from dataset
  4025. if (group.options.catmullRom.enabled == true) {
  4026. d = this._catmullRom(dataset, group);
  4027. }
  4028. else {
  4029. d = this._linear(dataset);
  4030. }
  4031. // append with points for fill and finalize the path
  4032. if (group.options.shaded.enabled == true) {
  4033. var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
  4034. var dFill;
  4035. if (group.options.shaded.orientation == 'top') {
  4036. dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
  4037. }
  4038. else {
  4039. dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
  4040. }
  4041. fillPath.setAttributeNS(null, "class", group.className + " fill");
  4042. fillPath.setAttributeNS(null, "d", dFill);
  4043. }
  4044. // copy properties to path for drawing.
  4045. path.setAttributeNS(null, "d", "M" + d);
  4046. // draw points
  4047. if (group.options.drawPoints.enabled == true) {
  4048. this._drawPoints(dataset, group, this.svgElements, this.svg);
  4049. }
  4050. }
  4051. }
  4052. };
  4053. /**
  4054. * draw the data points
  4055. *
  4056. * @param dataset
  4057. * @param JSONcontainer
  4058. * @param svg
  4059. * @param group
  4060. */
  4061. LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
  4062. if (offset === undefined) {offset = 0;}
  4063. for (var i = 0; i < dataset.length; i++) {
  4064. DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
  4065. }
  4066. };
  4067. /**
  4068. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  4069. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  4070. * the yAxis.
  4071. *
  4072. * @param datapoints
  4073. * @returns {Array}
  4074. * @private
  4075. */
  4076. LineGraph.prototype._preprocessData = function (datapoints, group) {
  4077. var extractedData = [];
  4078. var xValue, yValue;
  4079. var toScreen = this.body.util.toScreen;
  4080. var increment = 1;
  4081. var amountOfPoints = datapoints.length;
  4082. var yMin = datapoints[0].y;
  4083. var yMax = datapoints[0].y;
  4084. // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
  4085. // of width changing of the yAxis.
  4086. if (group.options.sampling == true) {
  4087. var xDistance = this.body.util.toGlobalScreen(datapoints[datapoints.length-1].x) - this.body.util.toGlobalScreen(datapoints[0].x);
  4088. var pointsPerPixel = amountOfPoints/xDistance;
  4089. increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1,Math.round(pointsPerPixel)));
  4090. }
  4091. for (var i = 0; i < amountOfPoints; i += increment) {
  4092. xValue = toScreen(datapoints[i].x) + this.width - 1;
  4093. yValue = datapoints[i].y;
  4094. extractedData.push({x: xValue, y: yValue});
  4095. yMin = yMin > yValue ? yValue : yMin;
  4096. yMax = yMax < yValue ? yValue : yMax;
  4097. }
  4098. // extractedData.sort(function (a,b) {return a.x - b.x;});
  4099. return {min: yMin, max: yMax, data: extractedData};
  4100. };
  4101. /**
  4102. * This uses the DataAxis object to generate the correct Y coordinate on the SVG window. It uses the
  4103. * util function toScreen to get the x coordinate from the timestamp.
  4104. *
  4105. * @param datapoints
  4106. * @param options
  4107. * @returns {Array}
  4108. * @private
  4109. */
  4110. LineGraph.prototype._convertYvalues = function (datapoints, group) {
  4111. var extractedData = [];
  4112. var xValue, yValue;
  4113. var axis = this.yAxisLeft;
  4114. var svgHeight = Number(this.svg.style.height.replace("px",""));
  4115. if (group.options.yAxisOrientation == 'right') {
  4116. axis = this.yAxisRight;
  4117. }
  4118. for (var i = 0; i < datapoints.length; i++) {
  4119. xValue = datapoints[i].x;
  4120. yValue = Math.round(axis.convertValue(datapoints[i].y));
  4121. extractedData.push({x: xValue, y: yValue});
  4122. }
  4123. group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
  4124. // extractedData.sort(function (a,b) {return a.x - b.x;});
  4125. return extractedData;
  4126. };
  4127. /**
  4128. * This uses an uniform parametrization of the CatmullRom algorithm:
  4129. * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
  4130. * @param data
  4131. * @returns {string}
  4132. * @private
  4133. */
  4134. LineGraph.prototype._catmullRomUniform = function(data) {
  4135. // catmull rom
  4136. var p0, p1, p2, p3, bp1, bp2;
  4137. var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  4138. var normalization = 1/6;
  4139. var length = data.length;
  4140. for (var i = 0; i < length - 1; i++) {
  4141. p0 = (i == 0) ? data[0] : data[i-1];
  4142. p1 = data[i];
  4143. p2 = data[i+1];
  4144. p3 = (i + 2 < length) ? data[i+2] : p2;
  4145. // Catmull-Rom to Cubic Bezier conversion matrix
  4146. // 0 1 0 0
  4147. // -1/6 1 1/6 0
  4148. // 0 1/6 1 -1/6
  4149. // 0 0 1 0
  4150. // bp0 = { x: p1.x, y: p1.y };
  4151. bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
  4152. bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
  4153. // bp0 = { x: p2.x, y: p2.y };
  4154. d += "C" +
  4155. bp1.x + "," +
  4156. bp1.y + " " +
  4157. bp2.x + "," +
  4158. bp2.y + " " +
  4159. p2.x + "," +
  4160. p2.y + " ";
  4161. }
  4162. return d;
  4163. };
  4164. /**
  4165. * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
  4166. * By default, the centripetal parameterization is used because this gives the nicest results.
  4167. * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
  4168. *
  4169. * One optimization can be used to reuse distances since this is a sliding window approach.
  4170. * @param data
  4171. * @returns {string}
  4172. * @private
  4173. */
  4174. LineGraph.prototype._catmullRom = function(data, group) {
  4175. var alpha = group.options.catmullRom.alpha;
  4176. if (alpha == 0 || alpha === undefined) {
  4177. return this._catmullRomUniform(data);
  4178. }
  4179. else {
  4180. var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
  4181. var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
  4182. var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  4183. var length = data.length;
  4184. for (var i = 0; i < length - 1; i++) {
  4185. p0 = (i == 0) ? data[0] : data[i-1];
  4186. p1 = data[i];
  4187. p2 = data[i+1];
  4188. p3 = (i + 2 < length) ? data[i+2] : p2;
  4189. d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
  4190. d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
  4191. d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
  4192. // Catmull-Rom to Cubic Bezier conversion matrix
  4193. //
  4194. // A = 2d1^2a + 3d1^a * d2^a + d3^2a
  4195. // B = 2d3^2a + 3d3^a * d2^a + d2^2a
  4196. //
  4197. // [ 0 1 0 0 ]
  4198. // [ -d2^2a/N A/N d1^2a/N 0 ]
  4199. // [ 0 d3^2a/M B/M -d2^2a/M ]
  4200. // [ 0 0 1 0 ]
  4201. // [ 0 1 0 0 ]
  4202. // [ -d2pow2a/N A/N d1pow2a/N 0 ]
  4203. // [ 0 d3pow2a/M B/M -d2pow2a/M ]
  4204. // [ 0 0 1 0 ]
  4205. d3powA = Math.pow(d3, alpha);
  4206. d3pow2A = Math.pow(d3,2*alpha);
  4207. d2powA = Math.pow(d2, alpha);
  4208. d2pow2A = Math.pow(d2,2*alpha);
  4209. d1powA = Math.pow(d1, alpha);
  4210. d1pow2A = Math.pow(d1,2*alpha);
  4211. A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
  4212. B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
  4213. N = 3*d1powA * (d1powA + d2powA);
  4214. if (N > 0) {N = 1 / N;}
  4215. M = 3*d3powA * (d3powA + d2powA);
  4216. if (M > 0) {M = 1 / M;}
  4217. bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
  4218. y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
  4219. bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
  4220. y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
  4221. if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
  4222. if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
  4223. d += "C" +
  4224. bp1.x + "," +
  4225. bp1.y + " " +
  4226. bp2.x + "," +
  4227. bp2.y + " " +
  4228. p2.x + "," +
  4229. p2.y + " ";
  4230. }
  4231. return d;
  4232. }
  4233. };
  4234. /**
  4235. * this generates the SVG path for a linear drawing between datapoints.
  4236. * @param data
  4237. * @returns {string}
  4238. * @private
  4239. */
  4240. LineGraph.prototype._linear = function(data) {
  4241. // linear
  4242. var d = "";
  4243. for (var i = 0; i < data.length; i++) {
  4244. if (i == 0) {
  4245. d += data[i].x + "," + data[i].y;
  4246. }
  4247. else {
  4248. d += " " + data[i].x + "," + data[i].y;
  4249. }
  4250. }
  4251. return d;
  4252. };
  4253. /**
  4254. * @constructor DataStep
  4255. * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
  4256. * end data point. The class itself determines the best scale (step size) based on the
  4257. * provided start Date, end Date, and minimumStep.
  4258. *
  4259. * If minimumStep is provided, the step size is chosen as close as possible
  4260. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4261. * provided, the scale is set to 1 DAY.
  4262. * The minimumStep should correspond with the onscreen size of about 6 characters
  4263. *
  4264. * Alternatively, you can set a scale by hand.
  4265. * After creation, you can initialize the class by executing first(). Then you
  4266. * can iterate from the start date to the end date via next(). You can check if
  4267. * the end date is reached with the function hasNext(). After each step, you can
  4268. * retrieve the current date via getCurrent().
  4269. * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
  4270. * days, to years.
  4271. *
  4272. * Version: 1.2
  4273. *
  4274. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  4275. * or new Date(2010, 9, 21, 23, 45, 00)
  4276. * @param {Date} [end] The end date
  4277. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4278. */
  4279. function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
  4280. // variables
  4281. this.current = 0;
  4282. this.autoScale = true;
  4283. this.stepIndex = 0;
  4284. this.step = 1;
  4285. this.scale = 1;
  4286. this.marginStart;
  4287. this.marginEnd;
  4288. this.majorSteps = [1, 2, 5, 10];
  4289. this.minorSteps = [0.25, 0.5, 1, 2];
  4290. this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
  4291. }
  4292. /**
  4293. * Set a new range
  4294. * If minimumStep is provided, the step size is chosen as close as possible
  4295. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4296. * provided, the scale is set to 1 DAY.
  4297. * The minimumStep should correspond with the onscreen size of about 6 characters
  4298. * @param {Number} [start] The start date and time.
  4299. * @param {Number} [end] The end date and time.
  4300. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4301. */
  4302. DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
  4303. this._start = start;
  4304. this._end = end;
  4305. if (this.autoScale) {
  4306. this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
  4307. }
  4308. this.setFirst();
  4309. };
  4310. /**
  4311. * Automatically determine the scale that bests fits the provided minimum step
  4312. * @param {Number} [minimumStep] The minimum step size in milliseconds
  4313. */
  4314. DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
  4315. // round to floor
  4316. var size = this._end - this._start;
  4317. var safeSize = size * 1.1;
  4318. var minimumStepValue = minimumStep * (safeSize / containerHeight);
  4319. var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
  4320. var minorStepIdx = -1;
  4321. var magnitudefactor = Math.pow(10,orderOfMagnitude);
  4322. var start = 0;
  4323. if (orderOfMagnitude < 0) {
  4324. start = orderOfMagnitude;
  4325. }
  4326. var solutionFound = false;
  4327. for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
  4328. magnitudefactor = Math.pow(10,i);
  4329. for (var j = 0; j < this.minorSteps.length; j++) {
  4330. var stepSize = magnitudefactor * this.minorSteps[j];
  4331. if (stepSize >= minimumStepValue) {
  4332. solutionFound = true;
  4333. minorStepIdx = j;
  4334. break;
  4335. }
  4336. }
  4337. if (solutionFound == true) {
  4338. break;
  4339. }
  4340. }
  4341. this.stepIndex = minorStepIdx;
  4342. this.scale = magnitudefactor;
  4343. this.step = magnitudefactor * this.minorSteps[minorStepIdx];
  4344. };
  4345. /**
  4346. * Set the range iterator to the start date.
  4347. */
  4348. DataStep.prototype.first = function() {
  4349. this.setFirst();
  4350. };
  4351. /**
  4352. * Round the current date to the first minor date value
  4353. * This must be executed once when the current date is set to start Date
  4354. */
  4355. DataStep.prototype.setFirst = function() {
  4356. var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
  4357. var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
  4358. this.marginEnd = this.roundToMinor(niceEnd);
  4359. this.marginStart = this.roundToMinor(niceStart);
  4360. this.marginRange = this.marginEnd - this.marginStart;
  4361. this.current = this.marginEnd;
  4362. };
  4363. DataStep.prototype.roundToMinor = function(value) {
  4364. var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
  4365. if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
  4366. return rounded + (this.scale * this.minorSteps[this.stepIndex]);
  4367. }
  4368. else {
  4369. return rounded;
  4370. }
  4371. }
  4372. /**
  4373. * Check if the there is a next step
  4374. * @return {boolean} true if the current date has not passed the end date
  4375. */
  4376. DataStep.prototype.hasNext = function () {
  4377. return (this.current >= this.marginStart);
  4378. };
  4379. /**
  4380. * Do the next step
  4381. */
  4382. DataStep.prototype.next = function() {
  4383. var prev = this.current;
  4384. this.current -= this.step;
  4385. // safety mechanism: if current time is still unchanged, move to the end
  4386. if (this.current == prev) {
  4387. this.current = this._end;
  4388. }
  4389. };
  4390. /**
  4391. * Do the next step
  4392. */
  4393. DataStep.prototype.previous = function() {
  4394. this.current += this.step;
  4395. this.marginEnd += this.step;
  4396. this.marginRange = this.marginEnd - this.marginStart;
  4397. };
  4398. /**
  4399. * Get the current datetime
  4400. * @return {Number} current The current date
  4401. */
  4402. DataStep.prototype.getCurrent = function() {
  4403. var toPrecision = '' + Number(this.current).toPrecision(5);
  4404. for (var i = toPrecision.length-1; i > 0; i--) {
  4405. if (toPrecision[i] == "0") {
  4406. toPrecision = toPrecision.slice(0,i);
  4407. }
  4408. else if (toPrecision[i] == "." || toPrecision[i] == ",") {
  4409. toPrecision = toPrecision.slice(0,i);
  4410. break;
  4411. }
  4412. else{
  4413. break;
  4414. }
  4415. }
  4416. return toPrecision;
  4417. };
  4418. /**
  4419. * Snap a date to a rounded value.
  4420. * The snap intervals are dependent on the current scale and step.
  4421. * @param {Date} date the date to be snapped.
  4422. * @return {Date} snappedDate
  4423. */
  4424. DataStep.prototype.snap = function(date) {
  4425. };
  4426. /**
  4427. * Check if the current value is a major value (for example when the step
  4428. * is DAY, a major value is each first day of the MONTH)
  4429. * @return {boolean} true if current date is major, else false.
  4430. */
  4431. DataStep.prototype.isMajor = function() {
  4432. return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
  4433. };
  4434. /**
  4435. * Utility functions for ordering and stacking of items
  4436. */
  4437. var stack = {};
  4438. /**
  4439. * Order items by their start data
  4440. * @param {Item[]} items
  4441. */
  4442. stack.orderByStart = function(items) {
  4443. items.sort(function (a, b) {
  4444. return a.data.start - b.data.start;
  4445. });
  4446. };
  4447. /**
  4448. * Order items by their end date. If they have no end date, their start date
  4449. * is used.
  4450. * @param {Item[]} items
  4451. */
  4452. stack.orderByEnd = function(items) {
  4453. items.sort(function (a, b) {
  4454. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  4455. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  4456. return aTime - bTime;
  4457. });
  4458. };
  4459. /**
  4460. * Adjust vertical positions of the items such that they don't overlap each
  4461. * other.
  4462. * @param {Item[]} items
  4463. * All visible items
  4464. * @param {{item: number, axis: number}} margin
  4465. * Margins between items and between items and the axis.
  4466. * @param {boolean} [force=false]
  4467. * If true, all items will be repositioned. If false (default), only
  4468. * items having a top===null will be re-stacked
  4469. */
  4470. stack.stack = function(items, margin, force) {
  4471. var i, iMax;
  4472. if (force) {
  4473. // reset top position of all items
  4474. for (i = 0, iMax = items.length; i < iMax; i++) {
  4475. items[i].top = null;
  4476. }
  4477. }
  4478. // calculate new, non-overlapping positions
  4479. for (i = 0, iMax = items.length; i < iMax; i++) {
  4480. var item = items[i];
  4481. if (item.top === null) {
  4482. // initialize top position
  4483. item.top = margin.axis;
  4484. do {
  4485. // TODO: optimize checking for overlap. when there is a gap without items,
  4486. // you only need to check for items from the next item on, not from zero
  4487. var collidingItem = null;
  4488. for (var j = 0, jj = items.length; j < jj; j++) {
  4489. var other = items[j];
  4490. if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
  4491. collidingItem = other;
  4492. break;
  4493. }
  4494. }
  4495. if (collidingItem != null) {
  4496. // There is a collision. Reposition the items above the colliding element
  4497. item.top = collidingItem.top + collidingItem.height + margin.item;
  4498. }
  4499. } while (collidingItem);
  4500. }
  4501. }
  4502. };
  4503. /**
  4504. * Adjust vertical positions of the items without stacking them
  4505. * @param {Item[]} items
  4506. * All visible items
  4507. * @param {{item: number, axis: number}} margin
  4508. * Margins between items and between items and the axis.
  4509. */
  4510. stack.nostack = function(items, margin) {
  4511. var i, iMax;
  4512. // reset top position of all items
  4513. for (i = 0, iMax = items.length; i < iMax; i++) {
  4514. items[i].top = margin.axis;
  4515. }
  4516. };
  4517. /**
  4518. * Test if the two provided items collide
  4519. * The items must have parameters left, width, top, and height.
  4520. * @param {Item} a The first item
  4521. * @param {Item} b The second item
  4522. * @param {Number} margin A minimum required margin.
  4523. * If margin is provided, the two items will be
  4524. * marked colliding when they overlap or
  4525. * when the margin between the two is smaller than
  4526. * the requested margin.
  4527. * @return {boolean} true if a and b collide, else false
  4528. */
  4529. stack.collision = function(a, b, margin) {
  4530. return ((a.left - margin) < (b.left + b.width) &&
  4531. (a.left + a.width + margin) > b.left &&
  4532. (a.top - margin) < (b.top + b.height) &&
  4533. (a.top + a.height + margin) > b.top);
  4534. };
  4535. /**
  4536. * @constructor TimeStep
  4537. * The class TimeStep is an iterator for dates. You provide a start date and an
  4538. * end date. The class itself determines the best scale (step size) based on the
  4539. * provided start Date, end Date, and minimumStep.
  4540. *
  4541. * If minimumStep is provided, the step size is chosen as close as possible
  4542. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4543. * provided, the scale is set to 1 DAY.
  4544. * The minimumStep should correspond with the onscreen size of about 6 characters
  4545. *
  4546. * Alternatively, you can set a scale by hand.
  4547. * After creation, you can initialize the class by executing first(). Then you
  4548. * can iterate from the start date to the end date via next(). You can check if
  4549. * the end date is reached with the function hasNext(). After each step, you can
  4550. * retrieve the current date via getCurrent().
  4551. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  4552. * days, to years.
  4553. *
  4554. * Version: 1.2
  4555. *
  4556. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  4557. * or new Date(2010, 9, 21, 23, 45, 00)
  4558. * @param {Date} [end] The end date
  4559. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4560. */
  4561. function TimeStep(start, end, minimumStep) {
  4562. // variables
  4563. this.current = new Date();
  4564. this._start = new Date();
  4565. this._end = new Date();
  4566. this.autoScale = true;
  4567. this.scale = TimeStep.SCALE.DAY;
  4568. this.step = 1;
  4569. // initialize the range
  4570. this.setRange(start, end, minimumStep);
  4571. }
  4572. /// enum scale
  4573. TimeStep.SCALE = {
  4574. MILLISECOND: 1,
  4575. SECOND: 2,
  4576. MINUTE: 3,
  4577. HOUR: 4,
  4578. DAY: 5,
  4579. WEEKDAY: 6,
  4580. MONTH: 7,
  4581. YEAR: 8
  4582. };
  4583. /**
  4584. * Set a new range
  4585. * If minimumStep is provided, the step size is chosen as close as possible
  4586. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4587. * provided, the scale is set to 1 DAY.
  4588. * The minimumStep should correspond with the onscreen size of about 6 characters
  4589. * @param {Date} [start] The start date and time.
  4590. * @param {Date} [end] The end date and time.
  4591. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  4592. */
  4593. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  4594. if (!(start instanceof Date) || !(end instanceof Date)) {
  4595. throw "No legal start or end date in method setRange";
  4596. }
  4597. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  4598. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  4599. if (this.autoScale) {
  4600. this.setMinimumStep(minimumStep);
  4601. }
  4602. };
  4603. /**
  4604. * Set the range iterator to the start date.
  4605. */
  4606. TimeStep.prototype.first = function() {
  4607. this.current = new Date(this._start.valueOf());
  4608. this.roundToMinor();
  4609. };
  4610. /**
  4611. * Round the current date to the first minor date value
  4612. * This must be executed once when the current date is set to start Date
  4613. */
  4614. TimeStep.prototype.roundToMinor = function() {
  4615. // round to floor
  4616. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  4617. //noinspection FallthroughInSwitchStatementJS
  4618. switch (this.scale) {
  4619. case TimeStep.SCALE.YEAR:
  4620. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  4621. this.current.setMonth(0);
  4622. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  4623. case TimeStep.SCALE.DAY: // intentional fall through
  4624. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  4625. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  4626. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  4627. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  4628. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  4629. }
  4630. if (this.step != 1) {
  4631. // round down to the first minor value that is a multiple of the current step size
  4632. switch (this.scale) {
  4633. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  4634. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  4635. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  4636. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  4637. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4638. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  4639. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  4640. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  4641. default: break;
  4642. }
  4643. }
  4644. };
  4645. /**
  4646. * Check if the there is a next step
  4647. * @return {boolean} true if the current date has not passed the end date
  4648. */
  4649. TimeStep.prototype.hasNext = function () {
  4650. return (this.current.valueOf() <= this._end.valueOf());
  4651. };
  4652. /**
  4653. * Do the next step
  4654. */
  4655. TimeStep.prototype.next = function() {
  4656. var prev = this.current.valueOf();
  4657. // Two cases, needed to prevent issues with switching daylight savings
  4658. // (end of March and end of October)
  4659. if (this.current.getMonth() < 6) {
  4660. switch (this.scale) {
  4661. case TimeStep.SCALE.MILLISECOND:
  4662. this.current = new Date(this.current.valueOf() + this.step); break;
  4663. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  4664. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  4665. case TimeStep.SCALE.HOUR:
  4666. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  4667. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  4668. var h = this.current.getHours();
  4669. this.current.setHours(h - (h % this.step));
  4670. break;
  4671. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4672. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  4673. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  4674. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  4675. default: break;
  4676. }
  4677. }
  4678. else {
  4679. switch (this.scale) {
  4680. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  4681. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  4682. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  4683. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  4684. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4685. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  4686. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  4687. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  4688. default: break;
  4689. }
  4690. }
  4691. if (this.step != 1) {
  4692. // round down to the correct major value
  4693. switch (this.scale) {
  4694. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  4695. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  4696. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  4697. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  4698. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4699. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  4700. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  4701. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  4702. default: break;
  4703. }
  4704. }
  4705. // safety mechanism: if current time is still unchanged, move to the end
  4706. if (this.current.valueOf() == prev) {
  4707. this.current = new Date(this._end.valueOf());
  4708. }
  4709. };
  4710. /**
  4711. * Get the current datetime
  4712. * @return {Date} current The current date
  4713. */
  4714. TimeStep.prototype.getCurrent = function() {
  4715. return this.current;
  4716. };
  4717. /**
  4718. * Set a custom scale. Autoscaling will be disabled.
  4719. * For example setScale(SCALE.MINUTES, 5) will result
  4720. * in minor steps of 5 minutes, and major steps of an hour.
  4721. *
  4722. * @param {TimeStep.SCALE} newScale
  4723. * A scale. Choose from SCALE.MILLISECOND,
  4724. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  4725. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  4726. * SCALE.YEAR.
  4727. * @param {Number} newStep A step size, by default 1. Choose for
  4728. * example 1, 2, 5, or 10.
  4729. */
  4730. TimeStep.prototype.setScale = function(newScale, newStep) {
  4731. this.scale = newScale;
  4732. if (newStep > 0) {
  4733. this.step = newStep;
  4734. }
  4735. this.autoScale = false;
  4736. };
  4737. /**
  4738. * Enable or disable autoscaling
  4739. * @param {boolean} enable If true, autoascaling is set true
  4740. */
  4741. TimeStep.prototype.setAutoScale = function (enable) {
  4742. this.autoScale = enable;
  4743. };
  4744. /**
  4745. * Automatically determine the scale that bests fits the provided minimum step
  4746. * @param {Number} [minimumStep] The minimum step size in milliseconds
  4747. */
  4748. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  4749. if (minimumStep == undefined) {
  4750. return;
  4751. }
  4752. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  4753. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  4754. var stepDay = (1000 * 60 * 60 * 24);
  4755. var stepHour = (1000 * 60 * 60);
  4756. var stepMinute = (1000 * 60);
  4757. var stepSecond = (1000);
  4758. var stepMillisecond= (1);
  4759. // find the smallest step that is larger than the provided minimumStep
  4760. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  4761. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  4762. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  4763. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  4764. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  4765. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  4766. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  4767. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  4768. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  4769. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  4770. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  4771. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  4772. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  4773. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  4774. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  4775. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  4776. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  4777. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  4778. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  4779. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  4780. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  4781. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  4782. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  4783. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  4784. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  4785. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  4786. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  4787. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  4788. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  4789. };
  4790. /**
  4791. * Snap a date to a rounded value.
  4792. * The snap intervals are dependent on the current scale and step.
  4793. * @param {Date} date the date to be snapped.
  4794. * @return {Date} snappedDate
  4795. */
  4796. TimeStep.prototype.snap = function(date) {
  4797. var clone = new Date(date.valueOf());
  4798. if (this.scale == TimeStep.SCALE.YEAR) {
  4799. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  4800. clone.setFullYear(Math.round(year / this.step) * this.step);
  4801. clone.setMonth(0);
  4802. clone.setDate(0);
  4803. clone.setHours(0);
  4804. clone.setMinutes(0);
  4805. clone.setSeconds(0);
  4806. clone.setMilliseconds(0);
  4807. }
  4808. else if (this.scale == TimeStep.SCALE.MONTH) {
  4809. if (clone.getDate() > 15) {
  4810. clone.setDate(1);
  4811. clone.setMonth(clone.getMonth() + 1);
  4812. // important: first set Date to 1, after that change the month.
  4813. }
  4814. else {
  4815. clone.setDate(1);
  4816. }
  4817. clone.setHours(0);
  4818. clone.setMinutes(0);
  4819. clone.setSeconds(0);
  4820. clone.setMilliseconds(0);
  4821. }
  4822. else if (this.scale == TimeStep.SCALE.DAY) {
  4823. //noinspection FallthroughInSwitchStatementJS
  4824. switch (this.step) {
  4825. case 5:
  4826. case 2:
  4827. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  4828. default:
  4829. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  4830. }
  4831. clone.setMinutes(0);
  4832. clone.setSeconds(0);
  4833. clone.setMilliseconds(0);
  4834. }
  4835. else if (this.scale == TimeStep.SCALE.WEEKDAY) {
  4836. //noinspection FallthroughInSwitchStatementJS
  4837. switch (this.step) {
  4838. case 5:
  4839. case 2:
  4840. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  4841. default:
  4842. clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
  4843. }
  4844. clone.setMinutes(0);
  4845. clone.setSeconds(0);
  4846. clone.setMilliseconds(0);
  4847. }
  4848. else if (this.scale == TimeStep.SCALE.HOUR) {
  4849. switch (this.step) {
  4850. case 4:
  4851. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  4852. default:
  4853. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  4854. }
  4855. clone.setSeconds(0);
  4856. clone.setMilliseconds(0);
  4857. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  4858. //noinspection FallthroughInSwitchStatementJS
  4859. switch (this.step) {
  4860. case 15:
  4861. case 10:
  4862. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  4863. clone.setSeconds(0);
  4864. break;
  4865. case 5:
  4866. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  4867. default:
  4868. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  4869. }
  4870. clone.setMilliseconds(0);
  4871. }
  4872. else if (this.scale == TimeStep.SCALE.SECOND) {
  4873. //noinspection FallthroughInSwitchStatementJS
  4874. switch (this.step) {
  4875. case 15:
  4876. case 10:
  4877. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  4878. clone.setMilliseconds(0);
  4879. break;
  4880. case 5:
  4881. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  4882. default:
  4883. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  4884. }
  4885. }
  4886. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  4887. var step = this.step > 5 ? this.step / 2 : 1;
  4888. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  4889. }
  4890. return clone;
  4891. };
  4892. /**
  4893. * Check if the current value is a major value (for example when the step
  4894. * is DAY, a major value is each first day of the MONTH)
  4895. * @return {boolean} true if current date is major, else false.
  4896. */
  4897. TimeStep.prototype.isMajor = function() {
  4898. switch (this.scale) {
  4899. case TimeStep.SCALE.MILLISECOND:
  4900. return (this.current.getMilliseconds() == 0);
  4901. case TimeStep.SCALE.SECOND:
  4902. return (this.current.getSeconds() == 0);
  4903. case TimeStep.SCALE.MINUTE:
  4904. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  4905. // Note: this is no bug. Major label is equal for both minute and hour scale
  4906. case TimeStep.SCALE.HOUR:
  4907. return (this.current.getHours() == 0);
  4908. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  4909. case TimeStep.SCALE.DAY:
  4910. return (this.current.getDate() == 1);
  4911. case TimeStep.SCALE.MONTH:
  4912. return (this.current.getMonth() == 0);
  4913. case TimeStep.SCALE.YEAR:
  4914. return false;
  4915. default:
  4916. return false;
  4917. }
  4918. };
  4919. /**
  4920. * Returns formatted text for the minor axislabel, depending on the current
  4921. * date and the scale. For example when scale is MINUTE, the current time is
  4922. * formatted as "hh:mm".
  4923. * @param {Date} [date] custom date. if not provided, current date is taken
  4924. */
  4925. TimeStep.prototype.getLabelMinor = function(date) {
  4926. if (date == undefined) {
  4927. date = this.current;
  4928. }
  4929. switch (this.scale) {
  4930. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  4931. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  4932. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  4933. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  4934. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  4935. case TimeStep.SCALE.DAY: return moment(date).format('D');
  4936. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  4937. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  4938. default: return '';
  4939. }
  4940. };
  4941. /**
  4942. * Returns formatted text for the major axis label, depending on the current
  4943. * date and the scale. For example when scale is MINUTE, the major scale is
  4944. * hours, and the hour will be formatted as "hh".
  4945. * @param {Date} [date] custom date. if not provided, current date is taken
  4946. */
  4947. TimeStep.prototype.getLabelMajor = function(date) {
  4948. if (date == undefined) {
  4949. date = this.current;
  4950. }
  4951. //noinspection FallthroughInSwitchStatementJS
  4952. switch (this.scale) {
  4953. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  4954. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  4955. case TimeStep.SCALE.MINUTE:
  4956. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  4957. case TimeStep.SCALE.WEEKDAY:
  4958. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  4959. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  4960. case TimeStep.SCALE.YEAR: return '';
  4961. default: return '';
  4962. }
  4963. };
  4964. /**
  4965. * @constructor Range
  4966. * A Range controls a numeric range with a start and end value.
  4967. * The Range adjusts the range based on mouse events or programmatic changes,
  4968. * and triggers events when the range is changing or has been changed.
  4969. * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
  4970. * @param {Object} [options] See description at Range.setOptions
  4971. */
  4972. function Range(body, options) {
  4973. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  4974. this.start = now.clone().add('days', -3).valueOf(); // Number
  4975. this.end = now.clone().add('days', 4).valueOf(); // Number
  4976. this.body = body;
  4977. // default options
  4978. this.defaultOptions = {
  4979. start: null,
  4980. end: null,
  4981. direction: 'horizontal', // 'horizontal' or 'vertical'
  4982. moveable: true,
  4983. zoomable: true,
  4984. min: null,
  4985. max: null,
  4986. zoomMin: 10, // milliseconds
  4987. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
  4988. };
  4989. this.options = util.extend({}, this.defaultOptions);
  4990. this.props = {
  4991. touch: {}
  4992. };
  4993. // drag listeners for dragging
  4994. this.body.emitter.on('dragstart', this._onDragStart.bind(this));
  4995. this.body.emitter.on('drag', this._onDrag.bind(this));
  4996. this.body.emitter.on('dragend', this._onDragEnd.bind(this));
  4997. // ignore dragging when holding
  4998. this.body.emitter.on('hold', this._onHold.bind(this));
  4999. // mouse wheel for zooming
  5000. this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
  5001. this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  5002. // pinch to zoom
  5003. this.body.emitter.on('touch', this._onTouch.bind(this));
  5004. this.body.emitter.on('pinch', this._onPinch.bind(this));
  5005. this.setOptions(options);
  5006. }
  5007. Range.prototype = new Component();
  5008. /**
  5009. * Set options for the range controller
  5010. * @param {Object} options Available options:
  5011. * {Number | Date | String} start Start date for the range
  5012. * {Number | Date | String} end End date for the range
  5013. * {Number} min Minimum value for start
  5014. * {Number} max Maximum value for end
  5015. * {Number} zoomMin Set a minimum value for
  5016. * (end - start).
  5017. * {Number} zoomMax Set a maximum value for
  5018. * (end - start).
  5019. * {Boolean} moveable Enable moving of the range
  5020. * by dragging. True by default
  5021. * {Boolean} zoomable Enable zooming of the range
  5022. * by pinching/scrolling. True by default
  5023. */
  5024. Range.prototype.setOptions = function (options) {
  5025. if (options) {
  5026. // copy the options that we know
  5027. var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
  5028. util.selectiveExtend(fields, this.options, options);
  5029. if ('start' in options || 'end' in options) {
  5030. // apply a new range. both start and end are optional
  5031. this.setRange(options.start, options.end);
  5032. }
  5033. }
  5034. };
  5035. /**
  5036. * Test whether direction has a valid value
  5037. * @param {String} direction 'horizontal' or 'vertical'
  5038. */
  5039. function validateDirection (direction) {
  5040. if (direction != 'horizontal' && direction != 'vertical') {
  5041. throw new TypeError('Unknown direction "' + direction + '". ' +
  5042. 'Choose "horizontal" or "vertical".');
  5043. }
  5044. }
  5045. /**
  5046. * Set a new start and end range
  5047. * @param {Number} [start]
  5048. * @param {Number} [end]
  5049. */
  5050. Range.prototype.setRange = function(start, end) {
  5051. var changed = this._applyRange(start, end);
  5052. if (changed) {
  5053. var params = {
  5054. start: new Date(this.start),
  5055. end: new Date(this.end)
  5056. };
  5057. this.body.emitter.emit('rangechange', params);
  5058. this.body.emitter.emit('rangechanged', params);
  5059. }
  5060. };
  5061. /**
  5062. * Set a new start and end range. This method is the same as setRange, but
  5063. * does not trigger a range change and range changed event, and it returns
  5064. * true when the range is changed
  5065. * @param {Number} [start]
  5066. * @param {Number} [end]
  5067. * @return {Boolean} changed
  5068. * @private
  5069. */
  5070. Range.prototype._applyRange = function(start, end) {
  5071. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  5072. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  5073. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  5074. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  5075. diff;
  5076. // check for valid number
  5077. if (isNaN(newStart) || newStart === null) {
  5078. throw new Error('Invalid start "' + start + '"');
  5079. }
  5080. if (isNaN(newEnd) || newEnd === null) {
  5081. throw new Error('Invalid end "' + end + '"');
  5082. }
  5083. // prevent start < end
  5084. if (newEnd < newStart) {
  5085. newEnd = newStart;
  5086. }
  5087. // prevent start < min
  5088. if (min !== null) {
  5089. if (newStart < min) {
  5090. diff = (min - newStart);
  5091. newStart += diff;
  5092. newEnd += diff;
  5093. // prevent end > max
  5094. if (max != null) {
  5095. if (newEnd > max) {
  5096. newEnd = max;
  5097. }
  5098. }
  5099. }
  5100. }
  5101. // prevent end > max
  5102. if (max !== null) {
  5103. if (newEnd > max) {
  5104. diff = (newEnd - max);
  5105. newStart -= diff;
  5106. newEnd -= diff;
  5107. // prevent start < min
  5108. if (min != null) {
  5109. if (newStart < min) {
  5110. newStart = min;
  5111. }
  5112. }
  5113. }
  5114. }
  5115. // prevent (end-start) < zoomMin
  5116. if (this.options.zoomMin !== null) {
  5117. var zoomMin = parseFloat(this.options.zoomMin);
  5118. if (zoomMin < 0) {
  5119. zoomMin = 0;
  5120. }
  5121. if ((newEnd - newStart) < zoomMin) {
  5122. if ((this.end - this.start) === zoomMin) {
  5123. // ignore this action, we are already zoomed to the minimum
  5124. newStart = this.start;
  5125. newEnd = this.end;
  5126. }
  5127. else {
  5128. // zoom to the minimum
  5129. diff = (zoomMin - (newEnd - newStart));
  5130. newStart -= diff / 2;
  5131. newEnd += diff / 2;
  5132. }
  5133. }
  5134. }
  5135. // prevent (end-start) > zoomMax
  5136. if (this.options.zoomMax !== null) {
  5137. var zoomMax = parseFloat(this.options.zoomMax);
  5138. if (zoomMax < 0) {
  5139. zoomMax = 0;
  5140. }
  5141. if ((newEnd - newStart) > zoomMax) {
  5142. if ((this.end - this.start) === zoomMax) {
  5143. // ignore this action, we are already zoomed to the maximum
  5144. newStart = this.start;
  5145. newEnd = this.end;
  5146. }
  5147. else {
  5148. // zoom to the maximum
  5149. diff = ((newEnd - newStart) - zoomMax);
  5150. newStart += diff / 2;
  5151. newEnd -= diff / 2;
  5152. }
  5153. }
  5154. }
  5155. var changed = (this.start != newStart || this.end != newEnd);
  5156. this.start = newStart;
  5157. this.end = newEnd;
  5158. return changed;
  5159. };
  5160. /**
  5161. * Retrieve the current range.
  5162. * @return {Object} An object with start and end properties
  5163. */
  5164. Range.prototype.getRange = function() {
  5165. return {
  5166. start: this.start,
  5167. end: this.end
  5168. };
  5169. };
  5170. /**
  5171. * Calculate the conversion offset and scale for current range, based on
  5172. * the provided width
  5173. * @param {Number} width
  5174. * @returns {{offset: number, scale: number}} conversion
  5175. */
  5176. Range.prototype.conversion = function (width) {
  5177. return Range.conversion(this.start, this.end, width);
  5178. };
  5179. /**
  5180. * Static method to calculate the conversion offset and scale for a range,
  5181. * based on the provided start, end, and width
  5182. * @param {Number} start
  5183. * @param {Number} end
  5184. * @param {Number} width
  5185. * @returns {{offset: number, scale: number}} conversion
  5186. */
  5187. Range.conversion = function (start, end, width) {
  5188. if (width != 0 && (end - start != 0)) {
  5189. return {
  5190. offset: start,
  5191. scale: width / (end - start)
  5192. }
  5193. }
  5194. else {
  5195. return {
  5196. offset: 0,
  5197. scale: 1
  5198. };
  5199. }
  5200. };
  5201. /**
  5202. * Start dragging horizontally or vertically
  5203. * @param {Event} event
  5204. * @private
  5205. */
  5206. Range.prototype._onDragStart = function(event) {
  5207. // only allow dragging when configured as movable
  5208. if (!this.options.moveable) return;
  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. this.props.touch.start = this.start;
  5213. this.props.touch.end = this.end;
  5214. if (this.body.dom.root) {
  5215. this.body.dom.root.style.cursor = 'move';
  5216. }
  5217. };
  5218. /**
  5219. * Perform dragging operation
  5220. * @param {Event} event
  5221. * @private
  5222. */
  5223. Range.prototype._onDrag = function (event) {
  5224. // only allow dragging when configured as movable
  5225. if (!this.options.moveable) return;
  5226. var direction = this.options.direction;
  5227. validateDirection(direction);
  5228. // refuse to drag when we where pinching to prevent the timeline make a jump
  5229. // when releasing the fingers in opposite order from the touch screen
  5230. if (!this.props.touch.allowDragging) return;
  5231. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  5232. interval = (this.props.touch.end - this.props.touch.start),
  5233. width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
  5234. diffRange = -delta / width * interval;
  5235. this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
  5236. this.body.emitter.emit('rangechange', {
  5237. start: new Date(this.start),
  5238. end: new Date(this.end)
  5239. });
  5240. };
  5241. /**
  5242. * Stop dragging operation
  5243. * @param {event} event
  5244. * @private
  5245. */
  5246. Range.prototype._onDragEnd = function (event) {
  5247. // only allow dragging when configured as movable
  5248. if (!this.options.moveable) return;
  5249. // refuse to drag when we where pinching to prevent the timeline make a jump
  5250. // when releasing the fingers in opposite order from the touch screen
  5251. if (!this.props.touch.allowDragging) return;
  5252. if (this.body.dom.root) {
  5253. this.body.dom.root.style.cursor = 'auto';
  5254. }
  5255. // fire a rangechanged event
  5256. this.body.emitter.emit('rangechanged', {
  5257. start: new Date(this.start),
  5258. end: new Date(this.end)
  5259. });
  5260. };
  5261. /**
  5262. * Event handler for mouse wheel event, used to zoom
  5263. * Code from http://adomas.org/javascript-mouse-wheel/
  5264. * @param {Event} event
  5265. * @private
  5266. */
  5267. Range.prototype._onMouseWheel = function(event) {
  5268. // only allow zooming when configured as zoomable and moveable
  5269. if (!(this.options.zoomable && this.options.moveable)) return;
  5270. // retrieve delta
  5271. var delta = 0;
  5272. if (event.wheelDelta) { /* IE/Opera. */
  5273. delta = event.wheelDelta / 120;
  5274. } else if (event.detail) { /* Mozilla case. */
  5275. // In Mozilla, sign of delta is different than in IE.
  5276. // Also, delta is multiple of 3.
  5277. delta = -event.detail / 3;
  5278. }
  5279. // If delta is nonzero, handle it.
  5280. // Basically, delta is now positive if wheel was scrolled up,
  5281. // and negative, if wheel was scrolled down.
  5282. if (delta) {
  5283. // perform the zoom action. Delta is normally 1 or -1
  5284. // adjust a negative delta such that zooming in with delta 0.1
  5285. // equals zooming out with a delta -0.1
  5286. var scale;
  5287. if (delta < 0) {
  5288. scale = 1 - (delta / 5);
  5289. }
  5290. else {
  5291. scale = 1 / (1 + (delta / 5)) ;
  5292. }
  5293. // calculate center, the date to zoom around
  5294. var gesture = util.fakeGesture(this, event),
  5295. pointer = getPointer(gesture.center, this.body.dom.center),
  5296. pointerDate = this._pointerToDate(pointer);
  5297. this.zoom(scale, pointerDate);
  5298. }
  5299. // Prevent default actions caused by mouse wheel
  5300. // (else the page and timeline both zoom and scroll)
  5301. event.preventDefault();
  5302. };
  5303. /**
  5304. * Start of a touch gesture
  5305. * @private
  5306. */
  5307. Range.prototype._onTouch = function (event) {
  5308. this.props.touch.start = this.start;
  5309. this.props.touch.end = this.end;
  5310. this.props.touch.allowDragging = true;
  5311. this.props.touch.center = null;
  5312. };
  5313. /**
  5314. * On start of a hold gesture
  5315. * @private
  5316. */
  5317. Range.prototype._onHold = function () {
  5318. this.props.touch.allowDragging = false;
  5319. };
  5320. /**
  5321. * Handle pinch event
  5322. * @param {Event} event
  5323. * @private
  5324. */
  5325. Range.prototype._onPinch = function (event) {
  5326. // only allow zooming when configured as zoomable and moveable
  5327. if (!(this.options.zoomable && this.options.moveable)) return;
  5328. this.props.touch.allowDragging = false;
  5329. if (event.gesture.touches.length > 1) {
  5330. if (!this.props.touch.center) {
  5331. this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
  5332. }
  5333. var scale = 1 / event.gesture.scale,
  5334. initDate = this._pointerToDate(this.props.touch.center);
  5335. // calculate new start and end
  5336. var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
  5337. var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
  5338. // apply new range
  5339. this.setRange(newStart, newEnd);
  5340. }
  5341. };
  5342. /**
  5343. * Helper function to calculate the center date for zooming
  5344. * @param {{x: Number, y: Number}} pointer
  5345. * @return {number} date
  5346. * @private
  5347. */
  5348. Range.prototype._pointerToDate = function (pointer) {
  5349. var conversion;
  5350. var direction = this.options.direction;
  5351. validateDirection(direction);
  5352. if (direction == 'horizontal') {
  5353. var width = this.body.domProps.center.width;
  5354. conversion = this.conversion(width);
  5355. return pointer.x / conversion.scale + conversion.offset;
  5356. }
  5357. else {
  5358. var height = this.body.domProps.center.height;
  5359. conversion = this.conversion(height);
  5360. return pointer.y / conversion.scale + conversion.offset;
  5361. }
  5362. };
  5363. /**
  5364. * Get the pointer location relative to the location of the dom element
  5365. * @param {{pageX: Number, pageY: Number}} touch
  5366. * @param {Element} element HTML DOM element
  5367. * @return {{x: Number, y: Number}} pointer
  5368. * @private
  5369. */
  5370. function getPointer (touch, element) {
  5371. return {
  5372. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  5373. y: touch.pageY - vis.util.getAbsoluteTop(element)
  5374. };
  5375. }
  5376. /**
  5377. * Zoom the range the given scale in or out. Start and end date will
  5378. * be adjusted, and the timeline will be redrawn. You can optionally give a
  5379. * date around which to zoom.
  5380. * For example, try scale = 0.9 or 1.1
  5381. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  5382. * values below 1 will zoom in.
  5383. * @param {Number} [center] Value representing a date around which will
  5384. * be zoomed.
  5385. */
  5386. Range.prototype.zoom = function(scale, center) {
  5387. // if centerDate is not provided, take it half between start Date and end Date
  5388. if (center == null) {
  5389. center = (this.start + this.end) / 2;
  5390. }
  5391. // calculate new start and end
  5392. var newStart = center + (this.start - center) * scale;
  5393. var newEnd = center + (this.end - center) * scale;
  5394. this.setRange(newStart, newEnd);
  5395. };
  5396. /**
  5397. * Move the range with a given delta to the left or right. Start and end
  5398. * value will be adjusted. For example, try delta = 0.1 or -0.1
  5399. * @param {Number} delta Moving amount. Positive value will move right,
  5400. * negative value will move left
  5401. */
  5402. Range.prototype.move = function(delta) {
  5403. // zoom start Date and end Date relative to the centerDate
  5404. var diff = (this.end - this.start);
  5405. // apply new values
  5406. var newStart = this.start + diff * delta;
  5407. var newEnd = this.end + diff * delta;
  5408. // TODO: reckon with min and max range
  5409. this.start = newStart;
  5410. this.end = newEnd;
  5411. };
  5412. /**
  5413. * Move the range to a new center point
  5414. * @param {Number} moveTo New center point of the range
  5415. */
  5416. Range.prototype.moveTo = function(moveTo) {
  5417. var center = (this.start + this.end) / 2;
  5418. var diff = center - moveTo;
  5419. // calculate new start and end
  5420. var newStart = this.start - diff;
  5421. var newEnd = this.end - diff;
  5422. this.setRange(newStart, newEnd);
  5423. };
  5424. /**
  5425. * Prototype for visual components
  5426. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]
  5427. * @param {Object} [options]
  5428. */
  5429. function Component (body, options) {
  5430. this.options = null;
  5431. this.props = null;
  5432. }
  5433. /**
  5434. * Set options for the component. The new options will be merged into the
  5435. * current options.
  5436. * @param {Object} options
  5437. */
  5438. Component.prototype.setOptions = function(options) {
  5439. if (options) {
  5440. util.extend(this.options, options);
  5441. }
  5442. };
  5443. /**
  5444. * Repaint the component
  5445. * @return {boolean} Returns true if the component is resized
  5446. */
  5447. Component.prototype.redraw = function() {
  5448. // should be implemented by the component
  5449. return false;
  5450. };
  5451. /**
  5452. * Destroy the component. Cleanup DOM and event listeners
  5453. */
  5454. Component.prototype.destroy = function() {
  5455. // should be implemented by the component
  5456. };
  5457. /**
  5458. * Test whether the component is resized since the last time _isResized() was
  5459. * called.
  5460. * @return {Boolean} Returns true if the component is resized
  5461. * @protected
  5462. */
  5463. Component.prototype._isResized = function() {
  5464. var resized = (this.props._previousWidth !== this.props.width ||
  5465. this.props._previousHeight !== this.props.height);
  5466. this.props._previousWidth = this.props.width;
  5467. this.props._previousHeight = this.props.height;
  5468. return resized;
  5469. };
  5470. /**
  5471. * A horizontal time axis
  5472. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
  5473. * @param {Object} [options] See TimeAxis.setOptions for the available
  5474. * options.
  5475. * @constructor TimeAxis
  5476. * @extends Component
  5477. */
  5478. function TimeAxis (body, options) {
  5479. this.dom = {
  5480. foreground: null,
  5481. majorLines: [],
  5482. majorTexts: [],
  5483. minorLines: [],
  5484. minorTexts: [],
  5485. redundant: {
  5486. majorLines: [],
  5487. majorTexts: [],
  5488. minorLines: [],
  5489. minorTexts: []
  5490. }
  5491. };
  5492. this.props = {
  5493. range: {
  5494. start: 0,
  5495. end: 0,
  5496. minimumStep: 0
  5497. },
  5498. lineTop: 0
  5499. };
  5500. this.defaultOptions = {
  5501. orientation: 'bottom', // supported: 'top', 'bottom'
  5502. // TODO: implement timeaxis orientations 'left' and 'right'
  5503. showMinorLabels: true,
  5504. showMajorLabels: true
  5505. };
  5506. this.options = util.extend({}, this.defaultOptions);
  5507. this.body = body;
  5508. // create the HTML DOM
  5509. this._create();
  5510. this.setOptions(options);
  5511. }
  5512. TimeAxis.prototype = new Component();
  5513. /**
  5514. * Set options for the TimeAxis.
  5515. * Parameters will be merged in current options.
  5516. * @param {Object} options Available options:
  5517. * {string} [orientation]
  5518. * {boolean} [showMinorLabels]
  5519. * {boolean} [showMajorLabels]
  5520. */
  5521. TimeAxis.prototype.setOptions = function(options) {
  5522. if (options) {
  5523. // copy all options that we know
  5524. util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
  5525. }
  5526. };
  5527. /**
  5528. * Create the HTML DOM for the TimeAxis
  5529. */
  5530. TimeAxis.prototype._create = function() {
  5531. this.dom.foreground = document.createElement('div');
  5532. this.dom.background = document.createElement('div');
  5533. this.dom.foreground.className = 'timeaxis foreground';
  5534. this.dom.background.className = 'timeaxis background';
  5535. };
  5536. /**
  5537. * Destroy the TimeAxis
  5538. */
  5539. TimeAxis.prototype.destroy = function() {
  5540. // remove from DOM
  5541. if (this.dom.foreground.parentNode) {
  5542. this.dom.foreground.parentNode.removeChild(this.dom.foreground);
  5543. }
  5544. if (this.dom.background.parentNode) {
  5545. this.dom.background.parentNode.removeChild(this.dom.background);
  5546. }
  5547. this.body = null;
  5548. };
  5549. /**
  5550. * Repaint the component
  5551. * @return {boolean} Returns true if the component is resized
  5552. */
  5553. TimeAxis.prototype.redraw = function () {
  5554. var options = this.options,
  5555. props = this.props,
  5556. foreground = this.dom.foreground,
  5557. background = this.dom.background;
  5558. // determine the correct parent DOM element (depending on option orientation)
  5559. var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom;
  5560. var parentChanged = (foreground.parentNode !== parent);
  5561. // calculate character width and height
  5562. this._calculateCharSize();
  5563. // TODO: recalculate sizes only needed when parent is resized or options is changed
  5564. var orientation = this.options.orientation,
  5565. showMinorLabels = this.options.showMinorLabels,
  5566. showMajorLabels = this.options.showMajorLabels;
  5567. // determine the width and height of the elemens for the axis
  5568. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  5569. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  5570. props.height = props.minorLabelHeight + props.majorLabelHeight;
  5571. props.width = foreground.offsetWidth;
  5572. props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
  5573. (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
  5574. props.minorLineWidth = 1; // TODO: really calculate width
  5575. props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
  5576. props.majorLineWidth = 1; // TODO: really calculate width
  5577. // take foreground and background offline while updating (is almost twice as fast)
  5578. var foregroundNextSibling = foreground.nextSibling;
  5579. var backgroundNextSibling = background.nextSibling;
  5580. foreground.parentNode && foreground.parentNode.removeChild(foreground);
  5581. background.parentNode && background.parentNode.removeChild(background);
  5582. foreground.style.height = this.props.height + 'px';
  5583. this._repaintLabels();
  5584. // put DOM online again (at the same place)
  5585. if (foregroundNextSibling) {
  5586. parent.insertBefore(foreground, foregroundNextSibling);
  5587. }
  5588. else {
  5589. parent.appendChild(foreground)
  5590. }
  5591. if (backgroundNextSibling) {
  5592. this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
  5593. }
  5594. else {
  5595. this.body.dom.backgroundVertical.appendChild(background)
  5596. }
  5597. return this._isResized() || parentChanged;
  5598. };
  5599. /**
  5600. * Repaint major and minor text labels and vertical grid lines
  5601. * @private
  5602. */
  5603. TimeAxis.prototype._repaintLabels = function () {
  5604. var orientation = this.options.orientation;
  5605. // calculate range and step (step such that we have space for 7 characters per label)
  5606. var start = util.convert(this.body.range.start, 'Number'),
  5607. end = util.convert(this.body.range.end, 'Number'),
  5608. minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
  5609. -this.body.util.toTime(0).valueOf();
  5610. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  5611. this.step = step;
  5612. // Move all DOM elements to a "redundant" list, where they
  5613. // can be picked for re-use, and clear the lists with lines and texts.
  5614. // At the end of the function _repaintLabels, left over elements will be cleaned up
  5615. var dom = this.dom;
  5616. dom.redundant.majorLines = dom.majorLines;
  5617. dom.redundant.majorTexts = dom.majorTexts;
  5618. dom.redundant.minorLines = dom.minorLines;
  5619. dom.redundant.minorTexts = dom.minorTexts;
  5620. dom.majorLines = [];
  5621. dom.majorTexts = [];
  5622. dom.minorLines = [];
  5623. dom.minorTexts = [];
  5624. step.first();
  5625. var xFirstMajorLabel = undefined;
  5626. var max = 0;
  5627. while (step.hasNext() && max < 1000) {
  5628. max++;
  5629. var cur = step.getCurrent(),
  5630. x = this.body.util.toScreen(cur),
  5631. isMajor = step.isMajor();
  5632. // TODO: lines must have a width, such that we can create css backgrounds
  5633. if (this.options.showMinorLabels) {
  5634. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  5635. }
  5636. if (isMajor && this.options.showMajorLabels) {
  5637. if (x > 0) {
  5638. if (xFirstMajorLabel == undefined) {
  5639. xFirstMajorLabel = x;
  5640. }
  5641. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  5642. }
  5643. this._repaintMajorLine(x, orientation);
  5644. }
  5645. else {
  5646. this._repaintMinorLine(x, orientation);
  5647. }
  5648. step.next();
  5649. }
  5650. // create a major label on the left when needed
  5651. if (this.options.showMajorLabels) {
  5652. var leftTime = this.body.util.toTime(0),
  5653. leftText = step.getLabelMajor(leftTime),
  5654. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  5655. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  5656. this._repaintMajorText(0, leftText, orientation);
  5657. }
  5658. }
  5659. // Cleanup leftover DOM elements from the redundant list
  5660. util.forEach(this.dom.redundant, function (arr) {
  5661. while (arr.length) {
  5662. var elem = arr.pop();
  5663. if (elem && elem.parentNode) {
  5664. elem.parentNode.removeChild(elem);
  5665. }
  5666. }
  5667. });
  5668. };
  5669. /**
  5670. * Create a minor label for the axis at position x
  5671. * @param {Number} x
  5672. * @param {String} text
  5673. * @param {String} orientation "top" or "bottom" (default)
  5674. * @private
  5675. */
  5676. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  5677. // reuse redundant label
  5678. var label = this.dom.redundant.minorTexts.shift();
  5679. if (!label) {
  5680. // create new label
  5681. var content = document.createTextNode('');
  5682. label = document.createElement('div');
  5683. label.appendChild(content);
  5684. label.className = 'text minor';
  5685. this.dom.foreground.appendChild(label);
  5686. }
  5687. this.dom.minorTexts.push(label);
  5688. label.childNodes[0].nodeValue = text;
  5689. label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
  5690. label.style.left = x + 'px';
  5691. //label.title = title; // TODO: this is a heavy operation
  5692. };
  5693. /**
  5694. * Create a Major label for the axis at position x
  5695. * @param {Number} x
  5696. * @param {String} text
  5697. * @param {String} orientation "top" or "bottom" (default)
  5698. * @private
  5699. */
  5700. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  5701. // reuse redundant label
  5702. var label = this.dom.redundant.majorTexts.shift();
  5703. if (!label) {
  5704. // create label
  5705. var content = document.createTextNode(text);
  5706. label = document.createElement('div');
  5707. label.className = 'text major';
  5708. label.appendChild(content);
  5709. this.dom.foreground.appendChild(label);
  5710. }
  5711. this.dom.majorTexts.push(label);
  5712. label.childNodes[0].nodeValue = text;
  5713. //label.title = title; // TODO: this is a heavy operation
  5714. label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
  5715. label.style.left = x + 'px';
  5716. };
  5717. /**
  5718. * Create a minor line for the axis at position x
  5719. * @param {Number} x
  5720. * @param {String} orientation "top" or "bottom" (default)
  5721. * @private
  5722. */
  5723. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  5724. // reuse redundant line
  5725. var line = this.dom.redundant.minorLines.shift();
  5726. if (!line) {
  5727. // create vertical line
  5728. line = document.createElement('div');
  5729. line.className = 'grid vertical minor';
  5730. this.dom.background.appendChild(line);
  5731. }
  5732. this.dom.minorLines.push(line);
  5733. var props = this.props;
  5734. if (orientation == 'top') {
  5735. line.style.top = props.majorLabelHeight + 'px';
  5736. }
  5737. else {
  5738. line.style.top = this.body.domProps.top.height + 'px';
  5739. }
  5740. line.style.height = props.minorLineHeight + 'px';
  5741. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  5742. };
  5743. /**
  5744. * Create a Major line for the axis at position x
  5745. * @param {Number} x
  5746. * @param {String} orientation "top" or "bottom" (default)
  5747. * @private
  5748. */
  5749. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  5750. // reuse redundant line
  5751. var line = this.dom.redundant.majorLines.shift();
  5752. if (!line) {
  5753. // create vertical line
  5754. line = document.createElement('DIV');
  5755. line.className = 'grid vertical major';
  5756. this.dom.background.appendChild(line);
  5757. }
  5758. this.dom.majorLines.push(line);
  5759. var props = this.props;
  5760. if (orientation == 'top') {
  5761. line.style.top = '0';
  5762. }
  5763. else {
  5764. line.style.top = this.body.domProps.top.height + 'px';
  5765. }
  5766. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  5767. line.style.height = props.majorLineHeight + 'px';
  5768. };
  5769. /**
  5770. * Determine the size of text on the axis (both major and minor axis).
  5771. * The size is calculated only once and then cached in this.props.
  5772. * @private
  5773. */
  5774. TimeAxis.prototype._calculateCharSize = function () {
  5775. // Note: We calculate char size with every redraw. Size may change, for
  5776. // example when any of the timelines parents had display:none for example.
  5777. // determine the char width and height on the minor axis
  5778. if (!this.dom.measureCharMinor) {
  5779. this.dom.measureCharMinor = document.createElement('DIV');
  5780. this.dom.measureCharMinor.className = 'text minor measure';
  5781. this.dom.measureCharMinor.style.position = 'absolute';
  5782. this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
  5783. this.dom.foreground.appendChild(this.dom.measureCharMinor);
  5784. }
  5785. this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
  5786. this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
  5787. // determine the char width and height on the major axis
  5788. if (!this.dom.measureCharMajor) {
  5789. this.dom.measureCharMajor = document.createElement('DIV');
  5790. this.dom.measureCharMajor.className = 'text minor measure';
  5791. this.dom.measureCharMajor.style.position = 'absolute';
  5792. this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
  5793. this.dom.foreground.appendChild(this.dom.measureCharMajor);
  5794. }
  5795. this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
  5796. this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
  5797. };
  5798. /**
  5799. * Snap a date to a rounded value.
  5800. * The snap intervals are dependent on the current scale and step.
  5801. * @param {Date} date the date to be snapped.
  5802. * @return {Date} snappedDate
  5803. */
  5804. TimeAxis.prototype.snap = function(date) {
  5805. return this.step.snap(date);
  5806. };
  5807. /**
  5808. * A current time bar
  5809. * @param {{range: Range, dom: Object, domProps: Object}} body
  5810. * @param {Object} [options] Available parameters:
  5811. * {Boolean} [showCurrentTime]
  5812. * @constructor CurrentTime
  5813. * @extends Component
  5814. */
  5815. function CurrentTime (body, options) {
  5816. this.body = body;
  5817. // default options
  5818. this.defaultOptions = {
  5819. showCurrentTime: true
  5820. };
  5821. this.options = util.extend({}, this.defaultOptions);
  5822. this._create();
  5823. this.setOptions(options);
  5824. }
  5825. CurrentTime.prototype = new Component();
  5826. /**
  5827. * Create the HTML DOM for the current time bar
  5828. * @private
  5829. */
  5830. CurrentTime.prototype._create = function() {
  5831. var bar = document.createElement('div');
  5832. bar.className = 'currenttime';
  5833. bar.style.position = 'absolute';
  5834. bar.style.top = '0px';
  5835. bar.style.height = '100%';
  5836. this.bar = bar;
  5837. };
  5838. /**
  5839. * Destroy the CurrentTime bar
  5840. */
  5841. CurrentTime.prototype.destroy = function () {
  5842. this.options.showCurrentTime = false;
  5843. this.redraw(); // will remove the bar from the DOM and stop refreshing
  5844. this.body = null;
  5845. };
  5846. /**
  5847. * Set options for the component. Options will be merged in current options.
  5848. * @param {Object} options Available parameters:
  5849. * {boolean} [showCurrentTime]
  5850. */
  5851. CurrentTime.prototype.setOptions = function(options) {
  5852. if (options) {
  5853. // copy all options that we know
  5854. util.selectiveExtend(['showCurrentTime'], this.options, options);
  5855. }
  5856. };
  5857. /**
  5858. * Repaint the component
  5859. * @return {boolean} Returns true if the component is resized
  5860. */
  5861. CurrentTime.prototype.redraw = function() {
  5862. if (this.options.showCurrentTime) {
  5863. var parent = this.body.dom.backgroundVertical;
  5864. if (this.bar.parentNode != parent) {
  5865. // attach to the dom
  5866. if (this.bar.parentNode) {
  5867. this.bar.parentNode.removeChild(this.bar);
  5868. }
  5869. parent.appendChild(this.bar);
  5870. this.start();
  5871. }
  5872. var now = new Date();
  5873. var x = this.body.util.toScreen(now);
  5874. this.bar.style.left = x + 'px';
  5875. this.bar.title = 'Current time: ' + now;
  5876. }
  5877. else {
  5878. // remove the line from the DOM
  5879. if (this.bar.parentNode) {
  5880. this.bar.parentNode.removeChild(this.bar);
  5881. }
  5882. this.stop();
  5883. }
  5884. return false;
  5885. };
  5886. /**
  5887. * Start auto refreshing the current time bar
  5888. */
  5889. CurrentTime.prototype.start = function() {
  5890. var me = this;
  5891. function update () {
  5892. me.stop();
  5893. // determine interval to refresh
  5894. var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
  5895. var interval = 1 / scale / 10;
  5896. if (interval < 30) interval = 30;
  5897. if (interval > 1000) interval = 1000;
  5898. me.redraw();
  5899. // start a timer to adjust for the new time
  5900. me.currentTimeTimer = setTimeout(update, interval);
  5901. }
  5902. update();
  5903. };
  5904. /**
  5905. * Stop auto refreshing the current time bar
  5906. */
  5907. CurrentTime.prototype.stop = function() {
  5908. if (this.currentTimeTimer !== undefined) {
  5909. clearTimeout(this.currentTimeTimer);
  5910. delete this.currentTimeTimer;
  5911. }
  5912. };
  5913. /**
  5914. * A custom time bar
  5915. * @param {{range: Range, dom: Object}} body
  5916. * @param {Object} [options] Available parameters:
  5917. * {Boolean} [showCustomTime]
  5918. * @constructor CustomTime
  5919. * @extends Component
  5920. */
  5921. function CustomTime (body, options) {
  5922. this.body = body;
  5923. // default options
  5924. this.defaultOptions = {
  5925. showCustomTime: false
  5926. };
  5927. this.options = util.extend({}, this.defaultOptions);
  5928. this.customTime = new Date();
  5929. this.eventParams = {}; // stores state parameters while dragging the bar
  5930. // create the DOM
  5931. this._create();
  5932. this.setOptions(options);
  5933. }
  5934. CustomTime.prototype = new Component();
  5935. /**
  5936. * Set options for the component. Options will be merged in current options.
  5937. * @param {Object} options Available parameters:
  5938. * {boolean} [showCustomTime]
  5939. */
  5940. CustomTime.prototype.setOptions = function(options) {
  5941. if (options) {
  5942. // copy all options that we know
  5943. util.selectiveExtend(['showCustomTime'], this.options, options);
  5944. }
  5945. };
  5946. /**
  5947. * Create the DOM for the custom time
  5948. * @private
  5949. */
  5950. CustomTime.prototype._create = function() {
  5951. var bar = document.createElement('div');
  5952. bar.className = 'customtime';
  5953. bar.style.position = 'absolute';
  5954. bar.style.top = '0px';
  5955. bar.style.height = '100%';
  5956. this.bar = bar;
  5957. var drag = document.createElement('div');
  5958. drag.style.position = 'relative';
  5959. drag.style.top = '0px';
  5960. drag.style.left = '-10px';
  5961. drag.style.height = '100%';
  5962. drag.style.width = '20px';
  5963. bar.appendChild(drag);
  5964. // attach event listeners
  5965. this.hammer = Hammer(bar, {
  5966. prevent_default: true
  5967. });
  5968. this.hammer.on('dragstart', this._onDragStart.bind(this));
  5969. this.hammer.on('drag', this._onDrag.bind(this));
  5970. this.hammer.on('dragend', this._onDragEnd.bind(this));
  5971. };
  5972. /**
  5973. * Destroy the CustomTime bar
  5974. */
  5975. CustomTime.prototype.destroy = function () {
  5976. this.options.showCustomTime = false;
  5977. this.redraw(); // will remove the bar from the DOM
  5978. this.hammer.enable(false);
  5979. this.hammer = null;
  5980. this.body = null;
  5981. };
  5982. /**
  5983. * Repaint the component
  5984. * @return {boolean} Returns true if the component is resized
  5985. */
  5986. CustomTime.prototype.redraw = function () {
  5987. if (this.options.showCustomTime) {
  5988. var parent = this.body.dom.backgroundVertical;
  5989. if (this.bar.parentNode != parent) {
  5990. // attach to the dom
  5991. if (this.bar.parentNode) {
  5992. this.bar.parentNode.removeChild(this.bar);
  5993. }
  5994. parent.appendChild(this.bar);
  5995. }
  5996. var x = this.body.util.toScreen(this.customTime);
  5997. this.bar.style.left = x + 'px';
  5998. this.bar.title = 'Time: ' + this.customTime;
  5999. }
  6000. else {
  6001. // remove the line from the DOM
  6002. if (this.bar.parentNode) {
  6003. this.bar.parentNode.removeChild(this.bar);
  6004. }
  6005. }
  6006. return false;
  6007. };
  6008. /**
  6009. * Set custom time.
  6010. * @param {Date} time
  6011. */
  6012. CustomTime.prototype.setCustomTime = function(time) {
  6013. this.customTime = new Date(time.valueOf());
  6014. this.redraw();
  6015. };
  6016. /**
  6017. * Retrieve the current custom time.
  6018. * @return {Date} customTime
  6019. */
  6020. CustomTime.prototype.getCustomTime = function() {
  6021. return new Date(this.customTime.valueOf());
  6022. };
  6023. /**
  6024. * Start moving horizontally
  6025. * @param {Event} event
  6026. * @private
  6027. */
  6028. CustomTime.prototype._onDragStart = function(event) {
  6029. this.eventParams.dragging = true;
  6030. this.eventParams.customTime = this.customTime;
  6031. event.stopPropagation();
  6032. event.preventDefault();
  6033. };
  6034. /**
  6035. * Perform moving operating.
  6036. * @param {Event} event
  6037. * @private
  6038. */
  6039. CustomTime.prototype._onDrag = function (event) {
  6040. if (!this.eventParams.dragging) return;
  6041. var deltaX = event.gesture.deltaX,
  6042. x = this.body.util.toScreen(this.eventParams.customTime) + deltaX,
  6043. time = this.body.util.toTime(x);
  6044. this.setCustomTime(time);
  6045. // fire a timechange event
  6046. this.body.emitter.emit('timechange', {
  6047. time: new Date(this.customTime.valueOf())
  6048. });
  6049. event.stopPropagation();
  6050. event.preventDefault();
  6051. };
  6052. /**
  6053. * Stop moving operating.
  6054. * @param {event} event
  6055. * @private
  6056. */
  6057. CustomTime.prototype._onDragEnd = function (event) {
  6058. if (!this.eventParams.dragging) return;
  6059. // fire a timechanged event
  6060. this.body.emitter.emit('timechanged', {
  6061. time: new Date(this.customTime.valueOf())
  6062. });
  6063. event.stopPropagation();
  6064. event.preventDefault();
  6065. };
  6066. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  6067. /**
  6068. * An ItemSet holds a set of items and ranges which can be displayed in a
  6069. * range. The width is determined by the parent of the ItemSet, and the height
  6070. * is determined by the size of the items.
  6071. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
  6072. * @param {Object} [options] See ItemSet.setOptions for the available options.
  6073. * @constructor ItemSet
  6074. * @extends Component
  6075. */
  6076. function ItemSet(body, options) {
  6077. this.body = body;
  6078. this.defaultOptions = {
  6079. type: null, // 'box', 'point', 'range'
  6080. orientation: 'bottom', // 'top' or 'bottom'
  6081. align: 'center', // alignment of box items
  6082. stack: true,
  6083. groupOrder: null,
  6084. selectable: true,
  6085. editable: {
  6086. updateTime: false,
  6087. updateGroup: false,
  6088. add: false,
  6089. remove: false
  6090. },
  6091. onAdd: function (item, callback) {
  6092. callback(item);
  6093. },
  6094. onUpdate: function (item, callback) {
  6095. callback(item);
  6096. },
  6097. onMove: function (item, callback) {
  6098. callback(item);
  6099. },
  6100. onRemove: function (item, callback) {
  6101. callback(item);
  6102. },
  6103. margin: {
  6104. item: 10,
  6105. axis: 20
  6106. },
  6107. padding: 5
  6108. };
  6109. // options is shared by this ItemSet and all its items
  6110. this.options = util.extend({}, this.defaultOptions);
  6111. // options for getting items from the DataSet with the correct type
  6112. this.itemOptions = {
  6113. type: {start: 'Date', end: 'Date'}
  6114. };
  6115. this.conversion = {
  6116. toScreen: body.util.toScreen,
  6117. toTime: body.util.toTime
  6118. };
  6119. this.dom = {};
  6120. this.props = {};
  6121. this.hammer = null;
  6122. var me = this;
  6123. this.itemsData = null; // DataSet
  6124. this.groupsData = null; // DataSet
  6125. // listeners for the DataSet of the items
  6126. this.itemListeners = {
  6127. 'add': function (event, params, senderId) {
  6128. me._onAdd(params.items);
  6129. },
  6130. 'update': function (event, params, senderId) {
  6131. me._onUpdate(params.items);
  6132. },
  6133. 'remove': function (event, params, senderId) {
  6134. me._onRemove(params.items);
  6135. }
  6136. };
  6137. // listeners for the DataSet of the groups
  6138. this.groupListeners = {
  6139. 'add': function (event, params, senderId) {
  6140. me._onAddGroups(params.items);
  6141. },
  6142. 'update': function (event, params, senderId) {
  6143. me._onUpdateGroups(params.items);
  6144. },
  6145. 'remove': function (event, params, senderId) {
  6146. me._onRemoveGroups(params.items);
  6147. }
  6148. };
  6149. this.items = {}; // object with an Item for every data item
  6150. this.groups = {}; // Group object for every group
  6151. this.groupIds = [];
  6152. this.selection = []; // list with the ids of all selected nodes
  6153. this.stackDirty = true; // if true, all items will be restacked on next redraw
  6154. this.touchParams = {}; // stores properties while dragging
  6155. // create the HTML DOM
  6156. this._create();
  6157. this.setOptions(options);
  6158. }
  6159. ItemSet.prototype = new Component();
  6160. // available item types will be registered here
  6161. ItemSet.types = {
  6162. box: ItemBox,
  6163. range: ItemRange,
  6164. point: ItemPoint
  6165. };
  6166. /**
  6167. * Create the HTML DOM for the ItemSet
  6168. */
  6169. ItemSet.prototype._create = function(){
  6170. var frame = document.createElement('div');
  6171. frame.className = 'itemset';
  6172. frame['timeline-itemset'] = this;
  6173. this.dom.frame = frame;
  6174. // create background panel
  6175. var background = document.createElement('div');
  6176. background.className = 'background';
  6177. frame.appendChild(background);
  6178. this.dom.background = background;
  6179. // create foreground panel
  6180. var foreground = document.createElement('div');
  6181. foreground.className = 'foreground';
  6182. frame.appendChild(foreground);
  6183. this.dom.foreground = foreground;
  6184. // create axis panel
  6185. var axis = document.createElement('div');
  6186. axis.className = 'axis';
  6187. this.dom.axis = axis;
  6188. // create labelset
  6189. var labelSet = document.createElement('div');
  6190. labelSet.className = 'labelset';
  6191. this.dom.labelSet = labelSet;
  6192. // create ungrouped Group
  6193. this._updateUngrouped();
  6194. // attach event listeners
  6195. // Note: we bind to the centerContainer for the case where the height
  6196. // of the center container is larger than of the ItemSet, so we
  6197. // can click in the empty area to create a new item or deselect an item.
  6198. this.hammer = Hammer(this.body.dom.centerContainer, {
  6199. prevent_default: true
  6200. });
  6201. // drag items when selected
  6202. this.hammer.on('touch', this._onTouch.bind(this));
  6203. this.hammer.on('dragstart', this._onDragStart.bind(this));
  6204. this.hammer.on('drag', this._onDrag.bind(this));
  6205. this.hammer.on('dragend', this._onDragEnd.bind(this));
  6206. // single select (or unselect) when tapping an item
  6207. this.hammer.on('tap', this._onSelectItem.bind(this));
  6208. // multi select when holding mouse/touch, or on ctrl+click
  6209. this.hammer.on('hold', this._onMultiSelectItem.bind(this));
  6210. // add item on doubletap
  6211. this.hammer.on('doubletap', this._onAddItem.bind(this));
  6212. // attach to the DOM
  6213. this.show();
  6214. };
  6215. /**
  6216. * Set options for the ItemSet. Existing options will be extended/overwritten.
  6217. * @param {Object} [options] The following options are available:
  6218. * {String} type
  6219. * Default type for the items. Choose from 'box'
  6220. * (default), 'point', or 'range'. The default
  6221. * Style can be overwritten by individual items.
  6222. * {String} align
  6223. * Alignment for the items, only applicable for
  6224. * ItemBox. Choose 'center' (default), 'left', or
  6225. * 'right'.
  6226. * {String} orientation
  6227. * Orientation of the item set. Choose 'top' or
  6228. * 'bottom' (default).
  6229. * {Function} groupOrder
  6230. * A sorting function for ordering groups
  6231. * {Boolean} stack
  6232. * If true (deafult), items will be stacked on
  6233. * top of each other.
  6234. * {Number} margin.axis
  6235. * Margin between the axis and the items in pixels.
  6236. * Default is 20.
  6237. * {Number} margin.item
  6238. * Margin between items in pixels. Default is 10.
  6239. * {Number} margin
  6240. * Set margin for both axis and items in pixels.
  6241. * {Number} padding
  6242. * Padding of the contents of an item in pixels.
  6243. * Must correspond with the items css. Default is 5.
  6244. * {Boolean} selectable
  6245. * If true (default), items can be selected.
  6246. * {Boolean} editable
  6247. * Set all editable options to true or false
  6248. * {Boolean} editable.updateTime
  6249. * Allow dragging an item to an other moment in time
  6250. * {Boolean} editable.updateGroup
  6251. * Allow dragging an item to an other group
  6252. * {Boolean} editable.add
  6253. * Allow creating new items on double tap
  6254. * {Boolean} editable.remove
  6255. * Allow removing items by clicking the delete button
  6256. * top right of a selected item.
  6257. * {Function(item: Item, callback: Function)} onAdd
  6258. * Callback function triggered when an item is about to be added:
  6259. * when the user double taps an empty space in the Timeline.
  6260. * {Function(item: Item, callback: Function)} onUpdate
  6261. * Callback function fired when an item is about to be updated.
  6262. * This function typically has to show a dialog where the user
  6263. * change the item. If not implemented, nothing happens.
  6264. * {Function(item: Item, callback: Function)} onMove
  6265. * Fired when an item has been moved. If not implemented,
  6266. * the move action will be accepted.
  6267. * {Function(item: Item, callback: Function)} onRemove
  6268. * Fired when an item is about to be deleted.
  6269. * If not implemented, the item will be always removed.
  6270. */
  6271. ItemSet.prototype.setOptions = function(options) {
  6272. if (options) {
  6273. // copy all options that we know
  6274. var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder'];
  6275. util.selectiveExtend(fields, this.options, options);
  6276. if ('margin' in options) {
  6277. if (typeof options.margin === 'number') {
  6278. this.options.margin.axis = options.margin;
  6279. this.options.margin.item = options.margin;
  6280. }
  6281. else if (typeof options.margin === 'object'){
  6282. util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin);
  6283. }
  6284. }
  6285. if ('editable' in options) {
  6286. if (typeof options.editable === 'boolean') {
  6287. this.options.editable.updateTime = options.editable;
  6288. this.options.editable.updateGroup = options.editable;
  6289. this.options.editable.add = options.editable;
  6290. this.options.editable.remove = options.editable;
  6291. }
  6292. else if (typeof options.editable === 'object') {
  6293. util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
  6294. }
  6295. }
  6296. // callback functions
  6297. var addCallback = (function (name) {
  6298. if (name in options) {
  6299. var fn = options[name];
  6300. if (!(fn instanceof Function) || fn.length != 2) {
  6301. throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
  6302. }
  6303. this.options[name] = fn;
  6304. }
  6305. }).bind(this);
  6306. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback);
  6307. // force the itemSet to refresh: options like orientation and margins may be changed
  6308. this.markDirty();
  6309. }
  6310. };
  6311. /**
  6312. * Mark the ItemSet dirty so it will refresh everything with next redraw
  6313. */
  6314. ItemSet.prototype.markDirty = function() {
  6315. this.groupIds = [];
  6316. this.stackDirty = true;
  6317. };
  6318. /**
  6319. * Destroy the ItemSet
  6320. */
  6321. ItemSet.prototype.destroy = function() {
  6322. this.hide();
  6323. this.setItems(null);
  6324. this.setGroups(null);
  6325. this.hammer = null;
  6326. this.body = null;
  6327. this.conversion = null;
  6328. };
  6329. /**
  6330. * Hide the component from the DOM
  6331. */
  6332. ItemSet.prototype.hide = function() {
  6333. // remove the frame containing the items
  6334. if (this.dom.frame.parentNode) {
  6335. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6336. }
  6337. // remove the axis with dots
  6338. if (this.dom.axis.parentNode) {
  6339. this.dom.axis.parentNode.removeChild(this.dom.axis);
  6340. }
  6341. // remove the labelset containing all group labels
  6342. if (this.dom.labelSet.parentNode) {
  6343. this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
  6344. }
  6345. };
  6346. /**
  6347. * Show the component in the DOM (when not already visible).
  6348. * @return {Boolean} changed
  6349. */
  6350. ItemSet.prototype.show = function() {
  6351. // show frame containing the items
  6352. if (!this.dom.frame.parentNode) {
  6353. this.body.dom.center.appendChild(this.dom.frame);
  6354. }
  6355. // show axis with dots
  6356. if (!this.dom.axis.parentNode) {
  6357. this.body.dom.backgroundVertical.appendChild(this.dom.axis);
  6358. }
  6359. // show labelset containing labels
  6360. if (!this.dom.labelSet.parentNode) {
  6361. this.body.dom.left.appendChild(this.dom.labelSet);
  6362. }
  6363. };
  6364. /**
  6365. * Set selected items by their id. Replaces the current selection
  6366. * Unknown id's are silently ignored.
  6367. * @param {Array} [ids] An array with zero or more id's of the items to be
  6368. * selected. If ids is an empty array, all items will be
  6369. * unselected.
  6370. */
  6371. ItemSet.prototype.setSelection = function(ids) {
  6372. var i, ii, id, item;
  6373. if (ids) {
  6374. if (!Array.isArray(ids)) {
  6375. throw new TypeError('Array expected');
  6376. }
  6377. // unselect currently selected items
  6378. for (i = 0, ii = this.selection.length; i < ii; i++) {
  6379. id = this.selection[i];
  6380. item = this.items[id];
  6381. if (item) item.unselect();
  6382. }
  6383. // select items
  6384. this.selection = [];
  6385. for (i = 0, ii = ids.length; i < ii; i++) {
  6386. id = ids[i];
  6387. item = this.items[id];
  6388. if (item) {
  6389. this.selection.push(id);
  6390. item.select();
  6391. }
  6392. }
  6393. }
  6394. };
  6395. /**
  6396. * Get the selected items by their id
  6397. * @return {Array} ids The ids of the selected items
  6398. */
  6399. ItemSet.prototype.getSelection = function() {
  6400. return this.selection.concat([]);
  6401. };
  6402. /**
  6403. * Deselect a selected item
  6404. * @param {String | Number} id
  6405. * @private
  6406. */
  6407. ItemSet.prototype._deselect = function(id) {
  6408. var selection = this.selection;
  6409. for (var i = 0, ii = selection.length; i < ii; i++) {
  6410. if (selection[i] == id) { // non-strict comparison!
  6411. selection.splice(i, 1);
  6412. break;
  6413. }
  6414. }
  6415. };
  6416. /**
  6417. * Repaint the component
  6418. * @return {boolean} Returns true if the component is resized
  6419. */
  6420. ItemSet.prototype.redraw = function() {
  6421. var margin = this.options.margin,
  6422. range = this.body.range,
  6423. asSize = util.option.asSize,
  6424. options = this.options,
  6425. orientation = options.orientation,
  6426. resized = false,
  6427. frame = this.dom.frame,
  6428. editable = options.editable.updateTime || options.editable.updateGroup;
  6429. // update class name
  6430. frame.className = 'itemset' + (editable ? ' editable' : '');
  6431. // reorder the groups (if needed)
  6432. resized = this._orderGroups() || resized;
  6433. // check whether zoomed (in that case we need to re-stack everything)
  6434. // TODO: would be nicer to get this as a trigger from Range
  6435. var visibleInterval = range.end - range.start;
  6436. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
  6437. if (zoomed) this.stackDirty = true;
  6438. this.lastVisibleInterval = visibleInterval;
  6439. this.props.lastWidth = this.props.width;
  6440. // redraw all groups
  6441. var restack = this.stackDirty,
  6442. firstGroup = this._firstGroup(),
  6443. firstMargin = {
  6444. item: margin.item,
  6445. axis: margin.axis
  6446. },
  6447. nonFirstMargin = {
  6448. item: margin.item,
  6449. axis: margin.item / 2
  6450. },
  6451. height = 0,
  6452. minHeight = margin.axis + margin.item;
  6453. util.forEach(this.groups, function (group) {
  6454. var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
  6455. var groupResized = group.redraw(range, groupMargin, restack);
  6456. resized = groupResized || resized;
  6457. height += group.height;
  6458. });
  6459. height = Math.max(height, minHeight);
  6460. this.stackDirty = false;
  6461. // update frame height
  6462. frame.style.height = asSize(height);
  6463. // calculate actual size and position
  6464. this.props.top = frame.offsetTop;
  6465. this.props.left = frame.offsetLeft;
  6466. this.props.width = frame.offsetWidth;
  6467. this.props.height = height;
  6468. // reposition axis
  6469. this.dom.axis.style.top = asSize((orientation == 'top') ?
  6470. (this.body.domProps.top.height + this.body.domProps.border.top) :
  6471. (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
  6472. this.dom.axis.style.left = this.body.domProps.border.left + 'px';
  6473. // check if this component is resized
  6474. resized = this._isResized() || resized;
  6475. return resized;
  6476. };
  6477. /**
  6478. * Get the first group, aligned with the axis
  6479. * @return {Group | null} firstGroup
  6480. * @private
  6481. */
  6482. ItemSet.prototype._firstGroup = function() {
  6483. var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
  6484. var firstGroupId = this.groupIds[firstGroupIndex];
  6485. var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
  6486. return firstGroup || null;
  6487. };
  6488. /**
  6489. * Create or delete the group holding all ungrouped items. This group is used when
  6490. * there are no groups specified.
  6491. * @protected
  6492. */
  6493. ItemSet.prototype._updateUngrouped = function() {
  6494. var ungrouped = this.groups[UNGROUPED];
  6495. if (this.groupsData) {
  6496. // remove the group holding all ungrouped items
  6497. if (ungrouped) {
  6498. ungrouped.hide();
  6499. delete this.groups[UNGROUPED];
  6500. }
  6501. }
  6502. else {
  6503. // create a group holding all (unfiltered) items
  6504. if (!ungrouped) {
  6505. var id = null;
  6506. var data = null;
  6507. ungrouped = new Group(id, data, this);
  6508. this.groups[UNGROUPED] = ungrouped;
  6509. for (var itemId in this.items) {
  6510. if (this.items.hasOwnProperty(itemId)) {
  6511. ungrouped.add(this.items[itemId]);
  6512. }
  6513. }
  6514. ungrouped.show();
  6515. }
  6516. }
  6517. };
  6518. /**
  6519. * Get the element for the labelset
  6520. * @return {HTMLElement} labelSet
  6521. */
  6522. ItemSet.prototype.getLabelSet = function() {
  6523. return this.dom.labelSet;
  6524. };
  6525. /**
  6526. * Set items
  6527. * @param {vis.DataSet | null} items
  6528. */
  6529. ItemSet.prototype.setItems = function(items) {
  6530. var me = this,
  6531. ids,
  6532. oldItemsData = this.itemsData;
  6533. // replace the dataset
  6534. if (!items) {
  6535. this.itemsData = null;
  6536. }
  6537. else if (items instanceof DataSet || items instanceof DataView) {
  6538. this.itemsData = items;
  6539. }
  6540. else {
  6541. throw new TypeError('Data must be an instance of DataSet or DataView');
  6542. }
  6543. if (oldItemsData) {
  6544. // unsubscribe from old dataset
  6545. util.forEach(this.itemListeners, function (callback, event) {
  6546. oldItemsData.off(event, callback);
  6547. });
  6548. // remove all drawn items
  6549. ids = oldItemsData.getIds();
  6550. this._onRemove(ids);
  6551. }
  6552. if (this.itemsData) {
  6553. // subscribe to new dataset
  6554. var id = this.id;
  6555. util.forEach(this.itemListeners, function (callback, event) {
  6556. me.itemsData.on(event, callback, id);
  6557. });
  6558. // add all new items
  6559. ids = this.itemsData.getIds();
  6560. this._onAdd(ids);
  6561. // update the group holding all ungrouped items
  6562. this._updateUngrouped();
  6563. }
  6564. };
  6565. /**
  6566. * Get the current items
  6567. * @returns {vis.DataSet | null}
  6568. */
  6569. ItemSet.prototype.getItems = function() {
  6570. return this.itemsData;
  6571. };
  6572. /**
  6573. * Set groups
  6574. * @param {vis.DataSet} groups
  6575. */
  6576. ItemSet.prototype.setGroups = function(groups) {
  6577. var me = this,
  6578. ids;
  6579. // unsubscribe from current dataset
  6580. if (this.groupsData) {
  6581. util.forEach(this.groupListeners, function (callback, event) {
  6582. me.groupsData.unsubscribe(event, callback);
  6583. });
  6584. // remove all drawn groups
  6585. ids = this.groupsData.getIds();
  6586. this.groupsData = null;
  6587. this._onRemoveGroups(ids); // note: this will cause a redraw
  6588. }
  6589. // replace the dataset
  6590. if (!groups) {
  6591. this.groupsData = null;
  6592. }
  6593. else if (groups instanceof DataSet || groups instanceof DataView) {
  6594. this.groupsData = groups;
  6595. }
  6596. else {
  6597. throw new TypeError('Data must be an instance of DataSet or DataView');
  6598. }
  6599. if (this.groupsData) {
  6600. // subscribe to new dataset
  6601. var id = this.id;
  6602. util.forEach(this.groupListeners, function (callback, event) {
  6603. me.groupsData.on(event, callback, id);
  6604. });
  6605. // draw all ms
  6606. ids = this.groupsData.getIds();
  6607. this._onAddGroups(ids);
  6608. }
  6609. // update the group holding all ungrouped items
  6610. this._updateUngrouped();
  6611. // update the order of all items in each group
  6612. this._order();
  6613. this.body.emitter.emit('change');
  6614. };
  6615. /**
  6616. * Get the current groups
  6617. * @returns {vis.DataSet | null} groups
  6618. */
  6619. ItemSet.prototype.getGroups = function() {
  6620. return this.groupsData;
  6621. };
  6622. /**
  6623. * Remove an item by its id
  6624. * @param {String | Number} id
  6625. */
  6626. ItemSet.prototype.removeItem = function(id) {
  6627. var item = this.itemsData.get(id),
  6628. dataset = this.itemsData.getDataSet();
  6629. if (item) {
  6630. // confirm deletion
  6631. this.options.onRemove(item, function (item) {
  6632. if (item) {
  6633. // remove by id here, it is possible that an item has no id defined
  6634. // itself, so better not delete by the item itself
  6635. dataset.remove(id);
  6636. }
  6637. });
  6638. }
  6639. };
  6640. /**
  6641. * Handle updated items
  6642. * @param {Number[]} ids
  6643. * @protected
  6644. */
  6645. ItemSet.prototype._onUpdate = function(ids) {
  6646. var me = this;
  6647. ids.forEach(function (id) {
  6648. var itemData = me.itemsData.get(id, me.itemOptions),
  6649. item = me.items[id],
  6650. type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box');
  6651. var constructor = ItemSet.types[type];
  6652. if (item) {
  6653. // update item
  6654. if (!constructor || !(item instanceof constructor)) {
  6655. // item type has changed, delete the item and recreate it
  6656. me._removeItem(item);
  6657. item = null;
  6658. }
  6659. else {
  6660. me._updateItem(item, itemData);
  6661. }
  6662. }
  6663. if (!item) {
  6664. // create item
  6665. if (constructor) {
  6666. item = new constructor(itemData, me.conversion, me.options);
  6667. item.id = id; // TODO: not so nice setting id afterwards
  6668. me._addItem(item);
  6669. }
  6670. else if (type == 'rangeoverflow') {
  6671. // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
  6672. throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
  6673. '.vis.timeline .item.range .content {overflow: visible;}');
  6674. }
  6675. else {
  6676. throw new TypeError('Unknown item type "' + type + '"');
  6677. }
  6678. }
  6679. });
  6680. this._order();
  6681. this.stackDirty = true; // force re-stacking of all items next redraw
  6682. this.body.emitter.emit('change');
  6683. };
  6684. /**
  6685. * Handle added items
  6686. * @param {Number[]} ids
  6687. * @protected
  6688. */
  6689. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  6690. /**
  6691. * Handle removed items
  6692. * @param {Number[]} ids
  6693. * @protected
  6694. */
  6695. ItemSet.prototype._onRemove = function(ids) {
  6696. var count = 0;
  6697. var me = this;
  6698. ids.forEach(function (id) {
  6699. var item = me.items[id];
  6700. if (item) {
  6701. count++;
  6702. me._removeItem(item);
  6703. }
  6704. });
  6705. if (count) {
  6706. // update order
  6707. this._order();
  6708. this.stackDirty = true; // force re-stacking of all items next redraw
  6709. this.body.emitter.emit('change');
  6710. }
  6711. };
  6712. /**
  6713. * Update the order of item in all groups
  6714. * @private
  6715. */
  6716. ItemSet.prototype._order = function() {
  6717. // reorder the items in all groups
  6718. // TODO: optimization: only reorder groups affected by the changed items
  6719. util.forEach(this.groups, function (group) {
  6720. group.order();
  6721. });
  6722. };
  6723. /**
  6724. * Handle updated groups
  6725. * @param {Number[]} ids
  6726. * @private
  6727. */
  6728. ItemSet.prototype._onUpdateGroups = function(ids) {
  6729. this._onAddGroups(ids);
  6730. };
  6731. /**
  6732. * Handle changed groups
  6733. * @param {Number[]} ids
  6734. * @private
  6735. */
  6736. ItemSet.prototype._onAddGroups = function(ids) {
  6737. var me = this;
  6738. ids.forEach(function (id) {
  6739. var groupData = me.groupsData.get(id);
  6740. var group = me.groups[id];
  6741. if (!group) {
  6742. // check for reserved ids
  6743. if (id == UNGROUPED) {
  6744. throw new Error('Illegal group id. ' + id + ' is a reserved id.');
  6745. }
  6746. var groupOptions = Object.create(me.options);
  6747. util.extend(groupOptions, {
  6748. height: null
  6749. });
  6750. group = new Group(id, groupData, me);
  6751. me.groups[id] = group;
  6752. // add items with this groupId to the new group
  6753. for (var itemId in me.items) {
  6754. if (me.items.hasOwnProperty(itemId)) {
  6755. var item = me.items[itemId];
  6756. if (item.data.group == id) {
  6757. group.add(item);
  6758. }
  6759. }
  6760. }
  6761. group.order();
  6762. group.show();
  6763. }
  6764. else {
  6765. // update group
  6766. group.setData(groupData);
  6767. }
  6768. });
  6769. this.body.emitter.emit('change');
  6770. };
  6771. /**
  6772. * Handle removed groups
  6773. * @param {Number[]} ids
  6774. * @private
  6775. */
  6776. ItemSet.prototype._onRemoveGroups = function(ids) {
  6777. var groups = this.groups;
  6778. ids.forEach(function (id) {
  6779. var group = groups[id];
  6780. if (group) {
  6781. group.hide();
  6782. delete groups[id];
  6783. }
  6784. });
  6785. this.markDirty();
  6786. this.body.emitter.emit('change');
  6787. };
  6788. /**
  6789. * Reorder the groups if needed
  6790. * @return {boolean} changed
  6791. * @private
  6792. */
  6793. ItemSet.prototype._orderGroups = function () {
  6794. if (this.groupsData) {
  6795. // reorder the groups
  6796. var groupIds = this.groupsData.getIds({
  6797. order: this.options.groupOrder
  6798. });
  6799. var changed = !util.equalArray(groupIds, this.groupIds);
  6800. if (changed) {
  6801. // hide all groups, removes them from the DOM
  6802. var groups = this.groups;
  6803. groupIds.forEach(function (groupId) {
  6804. groups[groupId].hide();
  6805. });
  6806. // show the groups again, attach them to the DOM in correct order
  6807. groupIds.forEach(function (groupId) {
  6808. groups[groupId].show();
  6809. });
  6810. this.groupIds = groupIds;
  6811. }
  6812. return changed;
  6813. }
  6814. else {
  6815. return false;
  6816. }
  6817. };
  6818. /**
  6819. * Add a new item
  6820. * @param {Item} item
  6821. * @private
  6822. */
  6823. ItemSet.prototype._addItem = function(item) {
  6824. this.items[item.id] = item;
  6825. // add to group
  6826. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  6827. var group = this.groups[groupId];
  6828. if (group) group.add(item);
  6829. };
  6830. /**
  6831. * Update an existing item
  6832. * @param {Item} item
  6833. * @param {Object} itemData
  6834. * @private
  6835. */
  6836. ItemSet.prototype._updateItem = function(item, itemData) {
  6837. var oldGroupId = item.data.group;
  6838. item.data = itemData;
  6839. if (item.displayed) {
  6840. item.redraw();
  6841. }
  6842. // update group
  6843. if (oldGroupId != item.data.group) {
  6844. var oldGroup = this.groups[oldGroupId];
  6845. if (oldGroup) oldGroup.remove(item);
  6846. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  6847. var group = this.groups[groupId];
  6848. if (group) group.add(item);
  6849. }
  6850. };
  6851. /**
  6852. * Delete an item from the ItemSet: remove it from the DOM, from the map
  6853. * with items, and from the map with visible items, and from the selection
  6854. * @param {Item} item
  6855. * @private
  6856. */
  6857. ItemSet.prototype._removeItem = function(item) {
  6858. // remove from DOM
  6859. item.hide();
  6860. // remove from items
  6861. delete this.items[item.id];
  6862. // remove from selection
  6863. var index = this.selection.indexOf(item.id);
  6864. if (index != -1) this.selection.splice(index, 1);
  6865. // remove from group
  6866. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  6867. var group = this.groups[groupId];
  6868. if (group) group.remove(item);
  6869. };
  6870. /**
  6871. * Create an array containing all items being a range (having an end date)
  6872. * @param array
  6873. * @returns {Array}
  6874. * @private
  6875. */
  6876. ItemSet.prototype._constructByEndArray = function(array) {
  6877. var endArray = [];
  6878. for (var i = 0; i < array.length; i++) {
  6879. if (array[i] instanceof ItemRange) {
  6880. endArray.push(array[i]);
  6881. }
  6882. }
  6883. return endArray;
  6884. };
  6885. /**
  6886. * Register the clicked item on touch, before dragStart is initiated.
  6887. *
  6888. * dragStart is initiated from a mousemove event, which can have left the item
  6889. * already resulting in an item == null
  6890. *
  6891. * @param {Event} event
  6892. * @private
  6893. */
  6894. ItemSet.prototype._onTouch = function (event) {
  6895. // store the touched item, used in _onDragStart
  6896. this.touchParams.item = ItemSet.itemFromTarget(event);
  6897. };
  6898. /**
  6899. * Start dragging the selected events
  6900. * @param {Event} event
  6901. * @private
  6902. */
  6903. ItemSet.prototype._onDragStart = function (event) {
  6904. if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
  6905. return;
  6906. }
  6907. var item = this.touchParams.item || null,
  6908. me = this,
  6909. props;
  6910. if (item && item.selected) {
  6911. var dragLeftItem = event.target.dragLeftItem;
  6912. var dragRightItem = event.target.dragRightItem;
  6913. if (dragLeftItem) {
  6914. props = {
  6915. item: dragLeftItem
  6916. };
  6917. if (me.options.editable.updateTime) {
  6918. props.start = item.data.start.valueOf();
  6919. }
  6920. if (me.options.editable.updateGroup) {
  6921. if ('group' in item.data) props.group = item.data.group;
  6922. }
  6923. this.touchParams.itemProps = [props];
  6924. }
  6925. else if (dragRightItem) {
  6926. props = {
  6927. item: dragRightItem
  6928. };
  6929. if (me.options.editable.updateTime) {
  6930. props.end = item.data.end.valueOf();
  6931. }
  6932. if (me.options.editable.updateGroup) {
  6933. if ('group' in item.data) props.group = item.data.group;
  6934. }
  6935. this.touchParams.itemProps = [props];
  6936. }
  6937. else {
  6938. this.touchParams.itemProps = this.getSelection().map(function (id) {
  6939. var item = me.items[id];
  6940. var props = {
  6941. item: item
  6942. };
  6943. if (me.options.editable.updateTime) {
  6944. if ('start' in item.data) props.start = item.data.start.valueOf();
  6945. if ('end' in item.data) props.end = item.data.end.valueOf();
  6946. }
  6947. if (me.options.editable.updateGroup) {
  6948. if ('group' in item.data) props.group = item.data.group;
  6949. }
  6950. return props;
  6951. });
  6952. }
  6953. event.stopPropagation();
  6954. }
  6955. };
  6956. /**
  6957. * Drag selected items
  6958. * @param {Event} event
  6959. * @private
  6960. */
  6961. ItemSet.prototype._onDrag = function (event) {
  6962. if (this.touchParams.itemProps) {
  6963. var range = this.body.range,
  6964. snap = this.body.util.snap || null,
  6965. deltaX = event.gesture.deltaX,
  6966. scale = (this.props.width / (range.end - range.start)),
  6967. offset = deltaX / scale;
  6968. // move
  6969. this.touchParams.itemProps.forEach(function (props) {
  6970. if ('start' in props) {
  6971. var start = new Date(props.start + offset);
  6972. props.item.data.start = snap ? snap(start) : start;
  6973. }
  6974. if ('end' in props) {
  6975. var end = new Date(props.end + offset);
  6976. props.item.data.end = snap ? snap(end) : end;
  6977. }
  6978. if ('group' in props) {
  6979. // drag from one group to another
  6980. var group = ItemSet.groupFromTarget(event);
  6981. if (group && group.groupId != props.item.data.group) {
  6982. var oldGroup = props.item.parent;
  6983. oldGroup.remove(props.item);
  6984. oldGroup.order();
  6985. group.add(props.item);
  6986. group.order();
  6987. props.item.data.group = group.groupId;
  6988. }
  6989. }
  6990. });
  6991. // TODO: implement onMoving handler
  6992. this.stackDirty = true; // force re-stacking of all items next redraw
  6993. this.body.emitter.emit('change');
  6994. event.stopPropagation();
  6995. }
  6996. };
  6997. /**
  6998. * End of dragging selected items
  6999. * @param {Event} event
  7000. * @private
  7001. */
  7002. ItemSet.prototype._onDragEnd = function (event) {
  7003. if (this.touchParams.itemProps) {
  7004. // prepare a change set for the changed items
  7005. var changes = [],
  7006. me = this,
  7007. dataset = this.itemsData.getDataSet();
  7008. this.touchParams.itemProps.forEach(function (props) {
  7009. var id = props.item.id,
  7010. itemData = me.itemsData.get(id, me.itemOptions);
  7011. var changed = false;
  7012. if ('start' in props.item.data) {
  7013. changed = (props.start != props.item.data.start.valueOf());
  7014. itemData.start = util.convert(props.item.data.start,
  7015. dataset._options.type && dataset._options.type.start || 'Date');
  7016. }
  7017. if ('end' in props.item.data) {
  7018. changed = changed || (props.end != props.item.data.end.valueOf());
  7019. itemData.end = util.convert(props.item.data.end,
  7020. dataset._options.type && dataset._options.type.end || 'Date');
  7021. }
  7022. if ('group' in props.item.data) {
  7023. changed = changed || (props.group != props.item.data.group);
  7024. itemData.group = props.item.data.group;
  7025. }
  7026. // only apply changes when start or end is actually changed
  7027. if (changed) {
  7028. me.options.onMove(itemData, function (itemData) {
  7029. if (itemData) {
  7030. // apply changes
  7031. itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
  7032. changes.push(itemData);
  7033. }
  7034. else {
  7035. // restore original values
  7036. if ('start' in props) props.item.data.start = props.start;
  7037. if ('end' in props) props.item.data.end = props.end;
  7038. me.stackDirty = true; // force re-stacking of all items next redraw
  7039. me.body.emitter.emit('change');
  7040. }
  7041. });
  7042. }
  7043. });
  7044. this.touchParams.itemProps = null;
  7045. // apply the changes to the data (if there are changes)
  7046. if (changes.length) {
  7047. dataset.update(changes);
  7048. }
  7049. event.stopPropagation();
  7050. }
  7051. };
  7052. /**
  7053. * Handle selecting/deselecting an item when tapping it
  7054. * @param {Event} event
  7055. * @private
  7056. */
  7057. ItemSet.prototype._onSelectItem = function (event) {
  7058. if (!this.options.selectable) return;
  7059. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  7060. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  7061. if (ctrlKey || shiftKey) {
  7062. this._onMultiSelectItem(event);
  7063. return;
  7064. }
  7065. var oldSelection = this.getSelection();
  7066. var item = ItemSet.itemFromTarget(event);
  7067. var selection = item ? [item.id] : [];
  7068. this.setSelection(selection);
  7069. var newSelection = this.getSelection();
  7070. // emit a select event,
  7071. // except when old selection is empty and new selection is still empty
  7072. if (newSelection.length > 0 || oldSelection.length > 0) {
  7073. this.body.emitter.emit('select', {
  7074. items: this.getSelection()
  7075. });
  7076. }
  7077. event.stopPropagation();
  7078. };
  7079. /**
  7080. * Handle creation and updates of an item on double tap
  7081. * @param event
  7082. * @private
  7083. */
  7084. ItemSet.prototype._onAddItem = function (event) {
  7085. if (!this.options.selectable) return;
  7086. if (!this.options.editable.add) return;
  7087. var me = this,
  7088. snap = this.body.util.snap || null,
  7089. item = ItemSet.itemFromTarget(event);
  7090. if (item) {
  7091. // update item
  7092. // execute async handler to update the item (or cancel it)
  7093. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  7094. this.options.onUpdate(itemData, function (itemData) {
  7095. if (itemData) {
  7096. me.itemsData.update(itemData);
  7097. }
  7098. });
  7099. }
  7100. else {
  7101. // add item
  7102. var xAbs = vis.util.getAbsoluteLeft(this.dom.frame);
  7103. var x = event.gesture.center.pageX - xAbs;
  7104. var start = this.body.util.toTime(x);
  7105. var newItem = {
  7106. start: snap ? snap(start) : start,
  7107. content: 'new item'
  7108. };
  7109. // when default type is a range, add a default end date to the new item
  7110. if (this.options.type === 'range') {
  7111. var end = this.body.util.toTime(x + this.props.width / 5);
  7112. newItem.end = snap ? snap(end) : end;
  7113. }
  7114. newItem[this.itemsData.fieldId] = util.randomUUID();
  7115. var group = ItemSet.groupFromTarget(event);
  7116. if (group) {
  7117. newItem.group = group.groupId;
  7118. }
  7119. // execute async handler to customize (or cancel) adding an item
  7120. this.options.onAdd(newItem, function (item) {
  7121. if (item) {
  7122. me.itemsData.add(newItem);
  7123. // TODO: need to trigger a redraw?
  7124. }
  7125. });
  7126. }
  7127. };
  7128. /**
  7129. * Handle selecting/deselecting multiple items when holding an item
  7130. * @param {Event} event
  7131. * @private
  7132. */
  7133. ItemSet.prototype._onMultiSelectItem = function (event) {
  7134. if (!this.options.selectable) return;
  7135. var selection,
  7136. item = ItemSet.itemFromTarget(event);
  7137. if (item) {
  7138. // multi select items
  7139. selection = this.getSelection(); // current selection
  7140. var index = selection.indexOf(item.id);
  7141. if (index == -1) {
  7142. // item is not yet selected -> select it
  7143. selection.push(item.id);
  7144. }
  7145. else {
  7146. // item is already selected -> deselect it
  7147. selection.splice(index, 1);
  7148. }
  7149. this.setSelection(selection);
  7150. this.body.emitter.emit('select', {
  7151. items: this.getSelection()
  7152. });
  7153. event.stopPropagation();
  7154. }
  7155. };
  7156. /**
  7157. * Find an item from an event target:
  7158. * searches for the attribute 'timeline-item' in the event target's element tree
  7159. * @param {Event} event
  7160. * @return {Item | null} item
  7161. */
  7162. ItemSet.itemFromTarget = function(event) {
  7163. var target = event.target;
  7164. while (target) {
  7165. if (target.hasOwnProperty('timeline-item')) {
  7166. return target['timeline-item'];
  7167. }
  7168. target = target.parentNode;
  7169. }
  7170. return null;
  7171. };
  7172. /**
  7173. * Find the Group from an event target:
  7174. * searches for the attribute 'timeline-group' in the event target's element tree
  7175. * @param {Event} event
  7176. * @return {Group | null} group
  7177. */
  7178. ItemSet.groupFromTarget = function(event) {
  7179. var target = event.target;
  7180. while (target) {
  7181. if (target.hasOwnProperty('timeline-group')) {
  7182. return target['timeline-group'];
  7183. }
  7184. target = target.parentNode;
  7185. }
  7186. return null;
  7187. };
  7188. /**
  7189. * Find the ItemSet from an event target:
  7190. * searches for the attribute 'timeline-itemset' in the event target's element tree
  7191. * @param {Event} event
  7192. * @return {ItemSet | null} item
  7193. */
  7194. ItemSet.itemSetFromTarget = function(event) {
  7195. var target = event.target;
  7196. while (target) {
  7197. if (target.hasOwnProperty('timeline-itemset')) {
  7198. return target['timeline-itemset'];
  7199. }
  7200. target = target.parentNode;
  7201. }
  7202. return null;
  7203. };
  7204. /**
  7205. * @constructor Item
  7206. * @param {Object} data Object containing (optional) parameters type,
  7207. * start, end, content, group, className.
  7208. * @param {{toScreen: function, toTime: function}} conversion
  7209. * Conversion functions from time to screen and vice versa
  7210. * @param {Object} options Configuration options
  7211. * // TODO: describe available options
  7212. */
  7213. function Item (data, conversion, options) {
  7214. this.id = null;
  7215. this.parent = null;
  7216. this.data = data;
  7217. this.dom = null;
  7218. this.conversion = conversion || {};
  7219. this.options = options || {};
  7220. this.selected = false;
  7221. this.displayed = false;
  7222. this.dirty = true;
  7223. this.top = null;
  7224. this.left = null;
  7225. this.width = null;
  7226. this.height = null;
  7227. }
  7228. /**
  7229. * Select current item
  7230. */
  7231. Item.prototype.select = function() {
  7232. this.selected = true;
  7233. if (this.displayed) this.redraw();
  7234. };
  7235. /**
  7236. * Unselect current item
  7237. */
  7238. Item.prototype.unselect = function() {
  7239. this.selected = false;
  7240. if (this.displayed) this.redraw();
  7241. };
  7242. /**
  7243. * Set a parent for the item
  7244. * @param {ItemSet | Group} parent
  7245. */
  7246. Item.prototype.setParent = function(parent) {
  7247. if (this.displayed) {
  7248. this.hide();
  7249. this.parent = parent;
  7250. if (this.parent) {
  7251. this.show();
  7252. }
  7253. }
  7254. else {
  7255. this.parent = parent;
  7256. }
  7257. };
  7258. /**
  7259. * Check whether this item is visible inside given range
  7260. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7261. * @returns {boolean} True if visible
  7262. */
  7263. Item.prototype.isVisible = function(range) {
  7264. // Should be implemented by Item implementations
  7265. return false;
  7266. };
  7267. /**
  7268. * Show the Item in the DOM (when not already visible)
  7269. * @return {Boolean} changed
  7270. */
  7271. Item.prototype.show = function() {
  7272. return false;
  7273. };
  7274. /**
  7275. * Hide the Item from the DOM (when visible)
  7276. * @return {Boolean} changed
  7277. */
  7278. Item.prototype.hide = function() {
  7279. return false;
  7280. };
  7281. /**
  7282. * Repaint the item
  7283. */
  7284. Item.prototype.redraw = function() {
  7285. // should be implemented by the item
  7286. };
  7287. /**
  7288. * Reposition the Item horizontally
  7289. */
  7290. Item.prototype.repositionX = function() {
  7291. // should be implemented by the item
  7292. };
  7293. /**
  7294. * Reposition the Item vertically
  7295. */
  7296. Item.prototype.repositionY = function() {
  7297. // should be implemented by the item
  7298. };
  7299. /**
  7300. * Repaint a delete button on the top right of the item when the item is selected
  7301. * @param {HTMLElement} anchor
  7302. * @protected
  7303. */
  7304. Item.prototype._repaintDeleteButton = function (anchor) {
  7305. if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
  7306. // create and show button
  7307. var me = this;
  7308. var deleteButton = document.createElement('div');
  7309. deleteButton.className = 'delete';
  7310. deleteButton.title = 'Delete this item';
  7311. Hammer(deleteButton, {
  7312. preventDefault: true
  7313. }).on('tap', function (event) {
  7314. me.parent.removeFromDataSet(me);
  7315. event.stopPropagation();
  7316. });
  7317. anchor.appendChild(deleteButton);
  7318. this.dom.deleteButton = deleteButton;
  7319. }
  7320. else if (!this.selected && this.dom.deleteButton) {
  7321. // remove button
  7322. if (this.dom.deleteButton.parentNode) {
  7323. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  7324. }
  7325. this.dom.deleteButton = null;
  7326. }
  7327. };
  7328. /**
  7329. * @constructor ItemBox
  7330. * @extends Item
  7331. * @param {Object} data Object containing parameters start
  7332. * content, className.
  7333. * @param {{toScreen: function, toTime: function}} conversion
  7334. * Conversion functions from time to screen and vice versa
  7335. * @param {Object} [options] Configuration options
  7336. * // TODO: describe available options
  7337. */
  7338. function ItemBox (data, conversion, options) {
  7339. this.props = {
  7340. dot: {
  7341. width: 0,
  7342. height: 0
  7343. },
  7344. line: {
  7345. width: 0,
  7346. height: 0
  7347. }
  7348. };
  7349. // validate data
  7350. if (data) {
  7351. if (data.start == undefined) {
  7352. throw new Error('Property "start" missing in item ' + data);
  7353. }
  7354. }
  7355. Item.call(this, data, conversion, options);
  7356. }
  7357. ItemBox.prototype = new Item (null, null, null);
  7358. /**
  7359. * Check whether this item is visible inside given range
  7360. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7361. * @returns {boolean} True if visible
  7362. */
  7363. ItemBox.prototype.isVisible = function(range) {
  7364. // determine visibility
  7365. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  7366. var interval = (range.end - range.start) / 4;
  7367. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  7368. };
  7369. /**
  7370. * Repaint the item
  7371. */
  7372. ItemBox.prototype.redraw = function() {
  7373. var dom = this.dom;
  7374. if (!dom) {
  7375. // create DOM
  7376. this.dom = {};
  7377. dom = this.dom;
  7378. // create main box
  7379. dom.box = document.createElement('DIV');
  7380. // contents box (inside the background box). used for making margins
  7381. dom.content = document.createElement('DIV');
  7382. dom.content.className = 'content';
  7383. dom.box.appendChild(dom.content);
  7384. // line to axis
  7385. dom.line = document.createElement('DIV');
  7386. dom.line.className = 'line';
  7387. // dot on axis
  7388. dom.dot = document.createElement('DIV');
  7389. dom.dot.className = 'dot';
  7390. // attach this item as attribute
  7391. dom.box['timeline-item'] = this;
  7392. }
  7393. // append DOM to parent DOM
  7394. if (!this.parent) {
  7395. throw new Error('Cannot redraw item: no parent attached');
  7396. }
  7397. if (!dom.box.parentNode) {
  7398. var foreground = this.parent.dom.foreground;
  7399. if (!foreground) throw new Error('Cannot redraw time axis: parent has no foreground container element');
  7400. foreground.appendChild(dom.box);
  7401. }
  7402. if (!dom.line.parentNode) {
  7403. var background = this.parent.dom.background;
  7404. if (!background) throw new Error('Cannot redraw time axis: parent has no background container element');
  7405. background.appendChild(dom.line);
  7406. }
  7407. if (!dom.dot.parentNode) {
  7408. var axis = this.parent.dom.axis;
  7409. if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
  7410. axis.appendChild(dom.dot);
  7411. }
  7412. this.displayed = true;
  7413. // update contents
  7414. if (this.data.content != this.content) {
  7415. this.content = this.data.content;
  7416. if (this.content instanceof Element) {
  7417. dom.content.innerHTML = '';
  7418. dom.content.appendChild(this.content);
  7419. }
  7420. else if (this.data.content != undefined) {
  7421. dom.content.innerHTML = this.content;
  7422. }
  7423. else {
  7424. throw new Error('Property "content" missing in item ' + this.data.id);
  7425. }
  7426. this.dirty = true;
  7427. }
  7428. // update title
  7429. if (this.data.title != this.title) {
  7430. dom.box.title = this.data.title;
  7431. this.title = this.data.title;
  7432. }
  7433. // update class
  7434. var className = (this.data.className? ' ' + this.data.className : '') +
  7435. (this.selected ? ' selected' : '');
  7436. if (this.className != className) {
  7437. this.className = className;
  7438. dom.box.className = 'item box' + className;
  7439. dom.line.className = 'item line' + className;
  7440. dom.dot.className = 'item dot' + className;
  7441. this.dirty = true;
  7442. }
  7443. // recalculate size
  7444. if (this.dirty) {
  7445. this.props.dot.height = dom.dot.offsetHeight;
  7446. this.props.dot.width = dom.dot.offsetWidth;
  7447. this.props.line.width = dom.line.offsetWidth;
  7448. this.width = dom.box.offsetWidth;
  7449. this.height = dom.box.offsetHeight;
  7450. this.dirty = false;
  7451. }
  7452. this._repaintDeleteButton(dom.box);
  7453. };
  7454. /**
  7455. * Show the item in the DOM (when not already displayed). The items DOM will
  7456. * be created when needed.
  7457. */
  7458. ItemBox.prototype.show = function() {
  7459. if (!this.displayed) {
  7460. this.redraw();
  7461. }
  7462. };
  7463. /**
  7464. * Hide the item from the DOM (when visible)
  7465. */
  7466. ItemBox.prototype.hide = function() {
  7467. if (this.displayed) {
  7468. var dom = this.dom;
  7469. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  7470. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  7471. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  7472. this.top = null;
  7473. this.left = null;
  7474. this.displayed = false;
  7475. }
  7476. };
  7477. /**
  7478. * Reposition the item horizontally
  7479. * @Override
  7480. */
  7481. ItemBox.prototype.repositionX = function() {
  7482. var start = this.conversion.toScreen(this.data.start),
  7483. align = this.options.align,
  7484. left,
  7485. box = this.dom.box,
  7486. line = this.dom.line,
  7487. dot = this.dom.dot;
  7488. // calculate left position of the box
  7489. if (align == 'right') {
  7490. this.left = start - this.width;
  7491. }
  7492. else if (align == 'left') {
  7493. this.left = start;
  7494. }
  7495. else {
  7496. // default or 'center'
  7497. this.left = start - this.width / 2;
  7498. }
  7499. // reposition box
  7500. box.style.left = this.left + 'px';
  7501. // reposition line
  7502. line.style.left = (start - this.props.line.width / 2) + 'px';
  7503. // reposition dot
  7504. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  7505. };
  7506. /**
  7507. * Reposition the item vertically
  7508. * @Override
  7509. */
  7510. ItemBox.prototype.repositionY = function() {
  7511. var orientation = this.options.orientation,
  7512. box = this.dom.box,
  7513. line = this.dom.line,
  7514. dot = this.dom.dot;
  7515. if (orientation == 'top') {
  7516. box.style.top = (this.top || 0) + 'px';
  7517. line.style.top = '0';
  7518. line.style.height = (this.parent.top + this.top + 1) + 'px';
  7519. line.style.bottom = '';
  7520. }
  7521. else { // orientation 'bottom'
  7522. var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
  7523. var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
  7524. box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
  7525. line.style.top = (itemSetHeight - lineHeight) + 'px';
  7526. line.style.bottom = '0';
  7527. }
  7528. dot.style.top = (-this.props.dot.height / 2) + 'px';
  7529. };
  7530. /**
  7531. * @constructor ItemPoint
  7532. * @extends Item
  7533. * @param {Object} data Object containing parameters start
  7534. * content, className.
  7535. * @param {{toScreen: function, toTime: function}} conversion
  7536. * Conversion functions from time to screen and vice versa
  7537. * @param {Object} [options] Configuration options
  7538. * // TODO: describe available options
  7539. */
  7540. function ItemPoint (data, conversion, options) {
  7541. this.props = {
  7542. dot: {
  7543. top: 0,
  7544. width: 0,
  7545. height: 0
  7546. },
  7547. content: {
  7548. height: 0,
  7549. marginLeft: 0
  7550. }
  7551. };
  7552. // validate data
  7553. if (data) {
  7554. if (data.start == undefined) {
  7555. throw new Error('Property "start" missing in item ' + data);
  7556. }
  7557. }
  7558. Item.call(this, data, conversion, options);
  7559. }
  7560. ItemPoint.prototype = new Item (null, null, null);
  7561. /**
  7562. * Check whether this item is visible inside given range
  7563. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7564. * @returns {boolean} True if visible
  7565. */
  7566. ItemPoint.prototype.isVisible = function(range) {
  7567. // determine visibility
  7568. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  7569. var interval = (range.end - range.start) / 4;
  7570. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  7571. };
  7572. /**
  7573. * Repaint the item
  7574. */
  7575. ItemPoint.prototype.redraw = function() {
  7576. var dom = this.dom;
  7577. if (!dom) {
  7578. // create DOM
  7579. this.dom = {};
  7580. dom = this.dom;
  7581. // background box
  7582. dom.point = document.createElement('div');
  7583. // className is updated in redraw()
  7584. // contents box, right from the dot
  7585. dom.content = document.createElement('div');
  7586. dom.content.className = 'content';
  7587. dom.point.appendChild(dom.content);
  7588. // dot at start
  7589. dom.dot = document.createElement('div');
  7590. dom.point.appendChild(dom.dot);
  7591. // attach this item as attribute
  7592. dom.point['timeline-item'] = this;
  7593. }
  7594. // append DOM to parent DOM
  7595. if (!this.parent) {
  7596. throw new Error('Cannot redraw item: no parent attached');
  7597. }
  7598. if (!dom.point.parentNode) {
  7599. var foreground = this.parent.dom.foreground;
  7600. if (!foreground) {
  7601. throw new Error('Cannot redraw time axis: parent has no foreground container element');
  7602. }
  7603. foreground.appendChild(dom.point);
  7604. }
  7605. this.displayed = true;
  7606. // update contents
  7607. if (this.data.content != this.content) {
  7608. this.content = this.data.content;
  7609. if (this.content instanceof Element) {
  7610. dom.content.innerHTML = '';
  7611. dom.content.appendChild(this.content);
  7612. }
  7613. else if (this.data.content != undefined) {
  7614. dom.content.innerHTML = this.content;
  7615. }
  7616. else {
  7617. throw new Error('Property "content" missing in item ' + this.data.id);
  7618. }
  7619. this.dirty = true;
  7620. }
  7621. // update title
  7622. if (this.data.title != this.title) {
  7623. dom.point.title = this.data.title;
  7624. this.title = this.data.title;
  7625. }
  7626. // update class
  7627. var className = (this.data.className? ' ' + this.data.className : '') +
  7628. (this.selected ? ' selected' : '');
  7629. if (this.className != className) {
  7630. this.className = className;
  7631. dom.point.className = 'item point' + className;
  7632. dom.dot.className = 'item dot' + className;
  7633. this.dirty = true;
  7634. }
  7635. // recalculate size
  7636. if (this.dirty) {
  7637. this.width = dom.point.offsetWidth;
  7638. this.height = dom.point.offsetHeight;
  7639. this.props.dot.width = dom.dot.offsetWidth;
  7640. this.props.dot.height = dom.dot.offsetHeight;
  7641. this.props.content.height = dom.content.offsetHeight;
  7642. // resize contents
  7643. dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
  7644. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  7645. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  7646. dom.dot.style.left = (this.props.dot.width / 2) + 'px';
  7647. this.dirty = false;
  7648. }
  7649. this._repaintDeleteButton(dom.point);
  7650. };
  7651. /**
  7652. * Show the item in the DOM (when not already visible). The items DOM will
  7653. * be created when needed.
  7654. */
  7655. ItemPoint.prototype.show = function() {
  7656. if (!this.displayed) {
  7657. this.redraw();
  7658. }
  7659. };
  7660. /**
  7661. * Hide the item from the DOM (when visible)
  7662. */
  7663. ItemPoint.prototype.hide = function() {
  7664. if (this.displayed) {
  7665. if (this.dom.point.parentNode) {
  7666. this.dom.point.parentNode.removeChild(this.dom.point);
  7667. }
  7668. this.top = null;
  7669. this.left = null;
  7670. this.displayed = false;
  7671. }
  7672. };
  7673. /**
  7674. * Reposition the item horizontally
  7675. * @Override
  7676. */
  7677. ItemPoint.prototype.repositionX = function() {
  7678. var start = this.conversion.toScreen(this.data.start);
  7679. this.left = start - this.props.dot.width;
  7680. // reposition point
  7681. this.dom.point.style.left = this.left + 'px';
  7682. };
  7683. /**
  7684. * Reposition the item vertically
  7685. * @Override
  7686. */
  7687. ItemPoint.prototype.repositionY = function() {
  7688. var orientation = this.options.orientation,
  7689. point = this.dom.point;
  7690. if (orientation == 'top') {
  7691. point.style.top = this.top + 'px';
  7692. }
  7693. else {
  7694. point.style.top = (this.parent.height - this.top - this.height) + 'px';
  7695. }
  7696. };
  7697. /**
  7698. * @constructor ItemRange
  7699. * @extends Item
  7700. * @param {Object} data Object containing parameters start, end
  7701. * content, className.
  7702. * @param {{toScreen: function, toTime: function}} conversion
  7703. * Conversion functions from time to screen and vice versa
  7704. * @param {Object} [options] Configuration options
  7705. * // TODO: describe options
  7706. */
  7707. function ItemRange (data, conversion, options) {
  7708. this.props = {
  7709. content: {
  7710. width: 0
  7711. }
  7712. };
  7713. this.overflow = false; // if contents can overflow (css styling), this flag is set to true
  7714. // validate data
  7715. if (data) {
  7716. if (data.start == undefined) {
  7717. throw new Error('Property "start" missing in item ' + data.id);
  7718. }
  7719. if (data.end == undefined) {
  7720. throw new Error('Property "end" missing in item ' + data.id);
  7721. }
  7722. }
  7723. Item.call(this, data, conversion, options);
  7724. }
  7725. ItemRange.prototype = new Item (null, null, null);
  7726. ItemRange.prototype.baseClassName = 'item range';
  7727. /**
  7728. * Check whether this item is visible inside given range
  7729. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  7730. * @returns {boolean} True if visible
  7731. */
  7732. ItemRange.prototype.isVisible = function(range) {
  7733. // determine visibility
  7734. return (this.data.start < range.end) && (this.data.end > range.start);
  7735. };
  7736. /**
  7737. * Repaint the item
  7738. */
  7739. ItemRange.prototype.redraw = function() {
  7740. var dom = this.dom;
  7741. if (!dom) {
  7742. // create DOM
  7743. this.dom = {};
  7744. dom = this.dom;
  7745. // background box
  7746. dom.box = document.createElement('div');
  7747. // className is updated in redraw()
  7748. // contents box
  7749. dom.content = document.createElement('div');
  7750. dom.content.className = 'content';
  7751. dom.box.appendChild(dom.content);
  7752. // attach this item as attribute
  7753. dom.box['timeline-item'] = this;
  7754. }
  7755. // append DOM to parent DOM
  7756. if (!this.parent) {
  7757. throw new Error('Cannot redraw item: no parent attached');
  7758. }
  7759. if (!dom.box.parentNode) {
  7760. var foreground = this.parent.dom.foreground;
  7761. if (!foreground) {
  7762. throw new Error('Cannot redraw time axis: parent has no foreground container element');
  7763. }
  7764. foreground.appendChild(dom.box);
  7765. }
  7766. this.displayed = true;
  7767. // update contents
  7768. if (this.data.content != this.content) {
  7769. this.content = this.data.content;
  7770. if (this.content instanceof Element) {
  7771. dom.content.innerHTML = '';
  7772. dom.content.appendChild(this.content);
  7773. }
  7774. else if (this.data.content != undefined) {
  7775. dom.content.innerHTML = this.content;
  7776. }
  7777. else {
  7778. throw new Error('Property "content" missing in item ' + this.data.id);
  7779. }
  7780. this.dirty = true;
  7781. }
  7782. // update title
  7783. if (this.data.title != this.title) {
  7784. dom.box.title = this.data.title;
  7785. this.title = this.data.title;
  7786. }
  7787. // update class
  7788. var className = (this.data.className ? (' ' + this.data.className) : '') +
  7789. (this.selected ? ' selected' : '');
  7790. if (this.className != className) {
  7791. this.className = className;
  7792. dom.box.className = this.baseClassName + className;
  7793. this.dirty = true;
  7794. }
  7795. // recalculate size
  7796. if (this.dirty) {
  7797. // determine from css whether this box has overflow
  7798. this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
  7799. this.props.content.width = this.dom.content.offsetWidth;
  7800. this.height = this.dom.box.offsetHeight;
  7801. this.dirty = false;
  7802. }
  7803. this._repaintDeleteButton(dom.box);
  7804. this._repaintDragLeft();
  7805. this._repaintDragRight();
  7806. };
  7807. /**
  7808. * Show the item in the DOM (when not already visible). The items DOM will
  7809. * be created when needed.
  7810. */
  7811. ItemRange.prototype.show = function() {
  7812. if (!this.displayed) {
  7813. this.redraw();
  7814. }
  7815. };
  7816. /**
  7817. * Hide the item from the DOM (when visible)
  7818. * @return {Boolean} changed
  7819. */
  7820. ItemRange.prototype.hide = function() {
  7821. if (this.displayed) {
  7822. var box = this.dom.box;
  7823. if (box.parentNode) {
  7824. box.parentNode.removeChild(box);
  7825. }
  7826. this.top = null;
  7827. this.left = null;
  7828. this.displayed = false;
  7829. }
  7830. };
  7831. /**
  7832. * Reposition the item horizontally
  7833. * @Override
  7834. */
  7835. // TODO: delete the old function
  7836. ItemRange.prototype.repositionX = function() {
  7837. var props = this.props,
  7838. parentWidth = this.parent.width,
  7839. start = this.conversion.toScreen(this.data.start),
  7840. end = this.conversion.toScreen(this.data.end),
  7841. padding = this.options.padding,
  7842. contentLeft;
  7843. // limit the width of the this, as browsers cannot draw very wide divs
  7844. if (start < -parentWidth) {
  7845. start = -parentWidth;
  7846. }
  7847. if (end > 2 * parentWidth) {
  7848. end = 2 * parentWidth;
  7849. }
  7850. var boxWidth = Math.max(end - start, 1);
  7851. if (this.overflow) {
  7852. // when range exceeds left of the window, position the contents at the left of the visible area
  7853. contentLeft = Math.max(-start, 0);
  7854. this.left = start;
  7855. this.width = boxWidth + this.props.content.width;
  7856. // Note: The calculation of width is an optimistic calculation, giving
  7857. // a width which will not change when moving the Timeline
  7858. // So no restacking needed, which is nicer for the eye;
  7859. }
  7860. else { // no overflow
  7861. // when range exceeds left of the window, position the contents at the left of the visible area
  7862. if (start < 0) {
  7863. contentLeft = Math.min(-start,
  7864. (end - start - props.content.width - 2 * padding));
  7865. // TODO: remove the need for options.padding. it's terrible.
  7866. }
  7867. else {
  7868. contentLeft = 0;
  7869. }
  7870. this.left = start;
  7871. this.width = boxWidth;
  7872. }
  7873. this.dom.box.style.left = this.left + 'px';
  7874. this.dom.box.style.width = boxWidth + 'px';
  7875. this.dom.content.style.left = contentLeft + 'px';
  7876. };
  7877. /**
  7878. * Reposition the item vertically
  7879. * @Override
  7880. */
  7881. ItemRange.prototype.repositionY = function() {
  7882. var orientation = this.options.orientation,
  7883. box = this.dom.box;
  7884. if (orientation == 'top') {
  7885. box.style.top = this.top + 'px';
  7886. }
  7887. else {
  7888. box.style.top = (this.parent.height - this.top - this.height) + 'px';
  7889. }
  7890. };
  7891. /**
  7892. * Repaint a drag area on the left side of the range when the range is selected
  7893. * @protected
  7894. */
  7895. ItemRange.prototype._repaintDragLeft = function () {
  7896. if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
  7897. // create and show drag area
  7898. var dragLeft = document.createElement('div');
  7899. dragLeft.className = 'drag-left';
  7900. dragLeft.dragLeftItem = this;
  7901. // TODO: this should be redundant?
  7902. Hammer(dragLeft, {
  7903. preventDefault: true
  7904. }).on('drag', function () {
  7905. //console.log('drag left')
  7906. });
  7907. this.dom.box.appendChild(dragLeft);
  7908. this.dom.dragLeft = dragLeft;
  7909. }
  7910. else if (!this.selected && this.dom.dragLeft) {
  7911. // delete drag area
  7912. if (this.dom.dragLeft.parentNode) {
  7913. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  7914. }
  7915. this.dom.dragLeft = null;
  7916. }
  7917. };
  7918. /**
  7919. * Repaint a drag area on the right side of the range when the range is selected
  7920. * @protected
  7921. */
  7922. ItemRange.prototype._repaintDragRight = function () {
  7923. if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
  7924. // create and show drag area
  7925. var dragRight = document.createElement('div');
  7926. dragRight.className = 'drag-right';
  7927. dragRight.dragRightItem = this;
  7928. // TODO: this should be redundant?
  7929. Hammer(dragRight, {
  7930. preventDefault: true
  7931. }).on('drag', function () {
  7932. //console.log('drag right')
  7933. });
  7934. this.dom.box.appendChild(dragRight);
  7935. this.dom.dragRight = dragRight;
  7936. }
  7937. else if (!this.selected && this.dom.dragRight) {
  7938. // delete drag area
  7939. if (this.dom.dragRight.parentNode) {
  7940. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  7941. }
  7942. this.dom.dragRight = null;
  7943. }
  7944. };
  7945. /**
  7946. * @constructor Group
  7947. * @param {Number | String} groupId
  7948. * @param {Object} data
  7949. * @param {ItemSet} itemSet
  7950. */
  7951. function Group (groupId, data, itemSet) {
  7952. this.groupId = groupId;
  7953. this.itemSet = itemSet;
  7954. this.dom = {};
  7955. this.props = {
  7956. label: {
  7957. width: 0,
  7958. height: 0
  7959. }
  7960. };
  7961. this.className = null;
  7962. this.items = {}; // items filtered by groupId of this group
  7963. this.visibleItems = []; // items currently visible in window
  7964. this.orderedItems = { // items sorted by start and by end
  7965. byStart: [],
  7966. byEnd: []
  7967. };
  7968. this._create();
  7969. this.setData(data);
  7970. }
  7971. /**
  7972. * Create DOM elements for the group
  7973. * @private
  7974. */
  7975. Group.prototype._create = function() {
  7976. var label = document.createElement('div');
  7977. label.className = 'vlabel';
  7978. this.dom.label = label;
  7979. var inner = document.createElement('div');
  7980. inner.className = 'inner';
  7981. label.appendChild(inner);
  7982. this.dom.inner = inner;
  7983. var foreground = document.createElement('div');
  7984. foreground.className = 'group';
  7985. foreground['timeline-group'] = this;
  7986. this.dom.foreground = foreground;
  7987. this.dom.background = document.createElement('div');
  7988. this.dom.background.className = 'group';
  7989. this.dom.axis = document.createElement('div');
  7990. this.dom.axis.className = 'group';
  7991. // create a hidden marker to detect when the Timelines container is attached
  7992. // to the DOM, or the style of a parent of the Timeline is changed from
  7993. // display:none is changed to visible.
  7994. this.dom.marker = document.createElement('div');
  7995. this.dom.marker.style.visibility = 'hidden';
  7996. this.dom.marker.innerHTML = '?';
  7997. this.dom.background.appendChild(this.dom.marker);
  7998. };
  7999. /**
  8000. * Set the group data for this group
  8001. * @param {Object} data Group data, can contain properties content and className
  8002. */
  8003. Group.prototype.setData = function(data) {
  8004. // update contents
  8005. var content = data && data.content;
  8006. if (content instanceof Element) {
  8007. this.dom.inner.appendChild(content);
  8008. }
  8009. else if (content != undefined) {
  8010. this.dom.inner.innerHTML = content;
  8011. }
  8012. else {
  8013. this.dom.inner.innerHTML = this.groupId;
  8014. }
  8015. // update title
  8016. this.dom.label.title = data && data.title || '';
  8017. if (!this.dom.inner.firstChild) {
  8018. util.addClassName(this.dom.inner, 'hidden');
  8019. }
  8020. else {
  8021. util.removeClassName(this.dom.inner, 'hidden');
  8022. }
  8023. // update className
  8024. var className = data && data.className || null;
  8025. if (className != this.className) {
  8026. if (this.className) {
  8027. util.removeClassName(this.dom.label, className);
  8028. util.removeClassName(this.dom.foreground, className);
  8029. util.removeClassName(this.dom.background, className);
  8030. util.removeClassName(this.dom.axis, className);
  8031. }
  8032. util.addClassName(this.dom.label, className);
  8033. util.addClassName(this.dom.foreground, className);
  8034. util.addClassName(this.dom.background, className);
  8035. util.addClassName(this.dom.axis, className);
  8036. }
  8037. };
  8038. /**
  8039. * Get the width of the group label
  8040. * @return {number} width
  8041. */
  8042. Group.prototype.getLabelWidth = function() {
  8043. return this.props.label.width;
  8044. };
  8045. /**
  8046. * Repaint this group
  8047. * @param {{start: number, end: number}} range
  8048. * @param {{item: number, axis: number}} margin
  8049. * @param {boolean} [restack=false] Force restacking of all items
  8050. * @return {boolean} Returns true if the group is resized
  8051. */
  8052. Group.prototype.redraw = function(range, margin, restack) {
  8053. var resized = false;
  8054. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  8055. // force recalculation of the height of the items when the marker height changed
  8056. // (due to the Timeline being attached to the DOM or changed from display:none to visible)
  8057. var markerHeight = this.dom.marker.clientHeight;
  8058. if (markerHeight != this.lastMarkerHeight) {
  8059. this.lastMarkerHeight = markerHeight;
  8060. util.forEach(this.items, function (item) {
  8061. item.dirty = true;
  8062. if (item.displayed) item.redraw();
  8063. });
  8064. restack = true;
  8065. }
  8066. // reposition visible items vertically
  8067. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  8068. stack.stack(this.visibleItems, margin, restack);
  8069. }
  8070. else { // no stacking
  8071. stack.nostack(this.visibleItems, margin);
  8072. }
  8073. // recalculate the height of the group
  8074. var height;
  8075. var visibleItems = this.visibleItems;
  8076. if (visibleItems.length) {
  8077. var min = visibleItems[0].top;
  8078. var max = visibleItems[0].top + visibleItems[0].height;
  8079. util.forEach(visibleItems, function (item) {
  8080. min = Math.min(min, item.top);
  8081. max = Math.max(max, (item.top + item.height));
  8082. });
  8083. if (min > margin.axis) {
  8084. // there is an empty gap between the lowest item and the axis
  8085. var offset = min - margin.axis;
  8086. max -= offset;
  8087. util.forEach(visibleItems, function (item) {
  8088. item.top -= offset;
  8089. });
  8090. }
  8091. height = max + margin.item / 2;
  8092. }
  8093. else {
  8094. height = margin.axis + margin.item;
  8095. }
  8096. height = Math.max(height, this.props.label.height);
  8097. // calculate actual size and position
  8098. var foreground = this.dom.foreground;
  8099. this.top = foreground.offsetTop;
  8100. this.left = foreground.offsetLeft;
  8101. this.width = foreground.offsetWidth;
  8102. resized = util.updateProperty(this, 'height', height) || resized;
  8103. // recalculate size of label
  8104. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  8105. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  8106. // apply new height
  8107. this.dom.background.style.height = height + 'px';
  8108. this.dom.foreground.style.height = height + 'px';
  8109. this.dom.label.style.height = height + 'px';
  8110. // update vertical position of items after they are re-stacked and the height of the group is calculated
  8111. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  8112. var item = this.visibleItems[i];
  8113. item.repositionY();
  8114. }
  8115. return resized;
  8116. };
  8117. /**
  8118. * Show this group: attach to the DOM
  8119. */
  8120. Group.prototype.show = function() {
  8121. if (!this.dom.label.parentNode) {
  8122. this.itemSet.dom.labelSet.appendChild(this.dom.label);
  8123. }
  8124. if (!this.dom.foreground.parentNode) {
  8125. this.itemSet.dom.foreground.appendChild(this.dom.foreground);
  8126. }
  8127. if (!this.dom.background.parentNode) {
  8128. this.itemSet.dom.background.appendChild(this.dom.background);
  8129. }
  8130. if (!this.dom.axis.parentNode) {
  8131. this.itemSet.dom.axis.appendChild(this.dom.axis);
  8132. }
  8133. };
  8134. /**
  8135. * Hide this group: remove from the DOM
  8136. */
  8137. Group.prototype.hide = function() {
  8138. var label = this.dom.label;
  8139. if (label.parentNode) {
  8140. label.parentNode.removeChild(label);
  8141. }
  8142. var foreground = this.dom.foreground;
  8143. if (foreground.parentNode) {
  8144. foreground.parentNode.removeChild(foreground);
  8145. }
  8146. var background = this.dom.background;
  8147. if (background.parentNode) {
  8148. background.parentNode.removeChild(background);
  8149. }
  8150. var axis = this.dom.axis;
  8151. if (axis.parentNode) {
  8152. axis.parentNode.removeChild(axis);
  8153. }
  8154. };
  8155. /**
  8156. * Add an item to the group
  8157. * @param {Item} item
  8158. */
  8159. Group.prototype.add = function(item) {
  8160. this.items[item.id] = item;
  8161. item.setParent(this);
  8162. if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
  8163. var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
  8164. this._checkIfVisible(item, this.visibleItems, range);
  8165. }
  8166. };
  8167. /**
  8168. * Remove an item from the group
  8169. * @param {Item} item
  8170. */
  8171. Group.prototype.remove = function(item) {
  8172. delete this.items[item.id];
  8173. item.setParent(this.itemSet);
  8174. // remove from visible items
  8175. var index = this.visibleItems.indexOf(item);
  8176. if (index != -1) this.visibleItems.splice(index, 1);
  8177. // TODO: also remove from ordered items?
  8178. };
  8179. /**
  8180. * Remove an item from the corresponding DataSet
  8181. * @param {Item} item
  8182. */
  8183. Group.prototype.removeFromDataSet = function(item) {
  8184. this.itemSet.removeItem(item.id);
  8185. };
  8186. /**
  8187. * Reorder the items
  8188. */
  8189. Group.prototype.order = function() {
  8190. var array = util.toArray(this.items);
  8191. this.orderedItems.byStart = array;
  8192. this.orderedItems.byEnd = this._constructByEndArray(array);
  8193. stack.orderByStart(this.orderedItems.byStart);
  8194. stack.orderByEnd(this.orderedItems.byEnd);
  8195. };
  8196. /**
  8197. * Create an array containing all items being a range (having an end date)
  8198. * @param {Item[]} array
  8199. * @returns {ItemRange[]}
  8200. * @private
  8201. */
  8202. Group.prototype._constructByEndArray = function(array) {
  8203. var endArray = [];
  8204. for (var i = 0; i < array.length; i++) {
  8205. if (array[i] instanceof ItemRange) {
  8206. endArray.push(array[i]);
  8207. }
  8208. }
  8209. return endArray;
  8210. };
  8211. /**
  8212. * Update the visible items
  8213. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  8214. * @param {Item[]} visibleItems The previously visible items.
  8215. * @param {{start: number, end: number}} range Visible range
  8216. * @return {Item[]} visibleItems The new visible items.
  8217. * @private
  8218. */
  8219. Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
  8220. var initialPosByStart,
  8221. newVisibleItems = [],
  8222. i;
  8223. // first check if the items that were in view previously are still in view.
  8224. // this handles the case for the ItemRange that is both before and after the current one.
  8225. if (visibleItems.length > 0) {
  8226. for (i = 0; i < visibleItems.length; i++) {
  8227. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  8228. }
  8229. }
  8230. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  8231. if (newVisibleItems.length == 0) {
  8232. initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
  8233. }
  8234. else {
  8235. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  8236. }
  8237. // use visible search to find a visible ItemRange (only based on endTime)
  8238. var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
  8239. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  8240. if (initialPosByStart != -1) {
  8241. for (i = initialPosByStart; i >= 0; i--) {
  8242. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  8243. }
  8244. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  8245. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  8246. }
  8247. }
  8248. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  8249. if (initialPosByEnd != -1) {
  8250. for (i = initialPosByEnd; i >= 0; i--) {
  8251. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  8252. }
  8253. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  8254. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  8255. }
  8256. }
  8257. return newVisibleItems;
  8258. };
  8259. /**
  8260. * this function checks if an item is invisible. If it is NOT we make it visible
  8261. * and add it to the global visible items. If it is, return true.
  8262. *
  8263. * @param {Item} item
  8264. * @param {Item[]} visibleItems
  8265. * @param {{start:number, end:number}} range
  8266. * @returns {boolean}
  8267. * @private
  8268. */
  8269. Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
  8270. if (item.isVisible(range)) {
  8271. if (!item.displayed) item.show();
  8272. item.repositionX();
  8273. if (visibleItems.indexOf(item) == -1) {
  8274. visibleItems.push(item);
  8275. }
  8276. return false;
  8277. }
  8278. else {
  8279. return true;
  8280. }
  8281. };
  8282. /**
  8283. * this function is very similar to the _checkIfInvisible() but it does not
  8284. * return booleans, hides the item if it should not be seen and always adds to
  8285. * the visibleItems.
  8286. * this one is for brute forcing and hiding.
  8287. *
  8288. * @param {Item} item
  8289. * @param {Array} visibleItems
  8290. * @param {{start:number, end:number}} range
  8291. * @private
  8292. */
  8293. Group.prototype._checkIfVisible = function(item, visibleItems, range) {
  8294. if (item.isVisible(range)) {
  8295. if (!item.displayed) item.show();
  8296. // reposition item horizontally
  8297. item.repositionX();
  8298. visibleItems.push(item);
  8299. }
  8300. else {
  8301. if (item.displayed) item.hide();
  8302. }
  8303. };
  8304. /**
  8305. * Create a timeline visualization
  8306. * @param {HTMLElement} container
  8307. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  8308. * @param {Object} [options] See Timeline.setOptions for the available options.
  8309. * @constructor
  8310. */
  8311. function Timeline (container, items, options) {
  8312. if (!(this instanceof Timeline)) {
  8313. throw new SyntaxError('Constructor must be called with the new operator');
  8314. }
  8315. var me = this;
  8316. this.defaultOptions = {
  8317. start: null,
  8318. end: null,
  8319. autoResize: true,
  8320. orientation: 'bottom',
  8321. width: null,
  8322. height: null,
  8323. maxHeight: null,
  8324. minHeight: null
  8325. };
  8326. this.options = util.deepExtend({}, this.defaultOptions);
  8327. // Create the DOM, props, and emitter
  8328. this._create(container);
  8329. // all components listed here will be repainted automatically
  8330. this.components = [];
  8331. this.body = {
  8332. dom: this.dom,
  8333. domProps: this.props,
  8334. emitter: {
  8335. on: this.on.bind(this),
  8336. off: this.off.bind(this),
  8337. emit: this.emit.bind(this)
  8338. },
  8339. util: {
  8340. snap: null, // will be specified after TimeAxis is created
  8341. toScreen: me._toScreen.bind(me),
  8342. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  8343. toTime: me._toTime.bind(me),
  8344. toGlobalTime : me._toGlobalTime.bind(me)
  8345. }
  8346. };
  8347. // range
  8348. this.range = new Range(this.body);
  8349. this.components.push(this.range);
  8350. this.body.range = this.range;
  8351. // time axis
  8352. this.timeAxis = new TimeAxis(this.body);
  8353. this.components.push(this.timeAxis);
  8354. this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
  8355. // current time bar
  8356. this.currentTime = new CurrentTime(this.body);
  8357. this.components.push(this.currentTime);
  8358. // custom time bar
  8359. // Note: time bar will be attached in this.setOptions when selected
  8360. this.customTime = new CustomTime(this.body);
  8361. this.components.push(this.customTime);
  8362. // item set
  8363. this.itemSet = new ItemSet(this.body);
  8364. this.components.push(this.itemSet);
  8365. this.itemsData = null; // DataSet
  8366. this.groupsData = null; // DataSet
  8367. // apply options
  8368. if (options) {
  8369. this.setOptions(options);
  8370. }
  8371. // create itemset
  8372. if (items) {
  8373. this.setItems(items);
  8374. }
  8375. else {
  8376. this.redraw();
  8377. }
  8378. }
  8379. // turn Timeline into an event emitter
  8380. Emitter(Timeline.prototype);
  8381. /**
  8382. * Create the main DOM for the Timeline: a root panel containing left, right,
  8383. * top, bottom, content, and background panel.
  8384. * @param {Element} container The container element where the Timeline will
  8385. * be attached.
  8386. * @private
  8387. */
  8388. Timeline.prototype._create = function (container) {
  8389. this.dom = {};
  8390. this.dom.root = document.createElement('div');
  8391. this.dom.background = document.createElement('div');
  8392. this.dom.backgroundVertical = document.createElement('div');
  8393. this.dom.backgroundHorizontal = document.createElement('div');
  8394. this.dom.centerContainer = document.createElement('div');
  8395. this.dom.leftContainer = document.createElement('div');
  8396. this.dom.rightContainer = document.createElement('div');
  8397. this.dom.center = document.createElement('div');
  8398. this.dom.left = document.createElement('div');
  8399. this.dom.right = document.createElement('div');
  8400. this.dom.top = document.createElement('div');
  8401. this.dom.bottom = document.createElement('div');
  8402. this.dom.shadowTop = document.createElement('div');
  8403. this.dom.shadowBottom = document.createElement('div');
  8404. this.dom.shadowTopLeft = document.createElement('div');
  8405. this.dom.shadowBottomLeft = document.createElement('div');
  8406. this.dom.shadowTopRight = document.createElement('div');
  8407. this.dom.shadowBottomRight = document.createElement('div');
  8408. this.dom.background.className = 'vispanel background';
  8409. this.dom.backgroundVertical.className = 'vispanel background vertical';
  8410. this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
  8411. this.dom.centerContainer.className = 'vispanel center';
  8412. this.dom.leftContainer.className = 'vispanel left';
  8413. this.dom.rightContainer.className = 'vispanel right';
  8414. this.dom.top.className = 'vispanel top';
  8415. this.dom.bottom.className = 'vispanel bottom';
  8416. this.dom.left.className = 'content';
  8417. this.dom.center.className = 'content';
  8418. this.dom.right.className = 'content';
  8419. this.dom.shadowTop.className = 'shadow top';
  8420. this.dom.shadowBottom.className = 'shadow bottom';
  8421. this.dom.shadowTopLeft.className = 'shadow top';
  8422. this.dom.shadowBottomLeft.className = 'shadow bottom';
  8423. this.dom.shadowTopRight.className = 'shadow top';
  8424. this.dom.shadowBottomRight.className = 'shadow bottom';
  8425. this.dom.root.appendChild(this.dom.background);
  8426. this.dom.root.appendChild(this.dom.backgroundVertical);
  8427. this.dom.root.appendChild(this.dom.backgroundHorizontal);
  8428. this.dom.root.appendChild(this.dom.centerContainer);
  8429. this.dom.root.appendChild(this.dom.leftContainer);
  8430. this.dom.root.appendChild(this.dom.rightContainer);
  8431. this.dom.root.appendChild(this.dom.top);
  8432. this.dom.root.appendChild(this.dom.bottom);
  8433. this.dom.centerContainer.appendChild(this.dom.center);
  8434. this.dom.leftContainer.appendChild(this.dom.left);
  8435. this.dom.rightContainer.appendChild(this.dom.right);
  8436. this.dom.centerContainer.appendChild(this.dom.shadowTop);
  8437. this.dom.centerContainer.appendChild(this.dom.shadowBottom);
  8438. this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
  8439. this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
  8440. this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
  8441. this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
  8442. this.on('rangechange', this.redraw.bind(this));
  8443. this.on('change', this.redraw.bind(this));
  8444. this.on('touch', this._onTouch.bind(this));
  8445. this.on('pinch', this._onPinch.bind(this));
  8446. this.on('dragstart', this._onDragStart.bind(this));
  8447. this.on('drag', this._onDrag.bind(this));
  8448. // create event listeners for all interesting events, these events will be
  8449. // emitted via emitter
  8450. this.hammer = Hammer(this.dom.root, {
  8451. prevent_default: true
  8452. });
  8453. this.listeners = {};
  8454. var me = this;
  8455. var events = [
  8456. 'touch', 'pinch',
  8457. 'tap', 'doubletap', 'hold',
  8458. 'dragstart', 'drag', 'dragend',
  8459. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
  8460. ];
  8461. events.forEach(function (event) {
  8462. var listener = function () {
  8463. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  8464. me.emit.apply(me, args);
  8465. };
  8466. me.hammer.on(event, listener);
  8467. me.listeners[event] = listener;
  8468. });
  8469. // size properties of each of the panels
  8470. this.props = {
  8471. root: {},
  8472. background: {},
  8473. centerContainer: {},
  8474. leftContainer: {},
  8475. rightContainer: {},
  8476. center: {},
  8477. left: {},
  8478. right: {},
  8479. top: {},
  8480. bottom: {},
  8481. border: {},
  8482. scrollTop: 0,
  8483. scrollTopMin: 0
  8484. };
  8485. this.touch = {}; // store state information needed for touch events
  8486. // attach the root panel to the provided container
  8487. if (!container) throw new Error('No container provided');
  8488. container.appendChild(this.dom.root);
  8489. };
  8490. /**
  8491. * Destroy the Timeline, clean up all DOM elements and event listeners.
  8492. */
  8493. Timeline.prototype.destroy = function () {
  8494. // unbind datasets
  8495. this.clear();
  8496. // remove all event listeners
  8497. this.off();
  8498. // stop checking for changed size
  8499. this._stopAutoResize();
  8500. // remove from DOM
  8501. if (this.dom.root.parentNode) {
  8502. this.dom.root.parentNode.removeChild(this.dom.root);
  8503. }
  8504. this.dom = null;
  8505. // cleanup hammer touch events
  8506. for (var event in this.listeners) {
  8507. if (this.listeners.hasOwnProperty(event)) {
  8508. delete this.listeners[event];
  8509. }
  8510. }
  8511. this.listeners = null;
  8512. this.hammer = null;
  8513. // give all components the opportunity to cleanup
  8514. this.components.forEach(function (component) {
  8515. component.destroy();
  8516. });
  8517. this.body = null;
  8518. };
  8519. /**
  8520. * Set options. Options will be passed to all components loaded in the Timeline.
  8521. * @param {Object} [options]
  8522. * {String} orientation
  8523. * Vertical orientation for the Timeline,
  8524. * can be 'bottom' (default) or 'top'.
  8525. * {String | Number} width
  8526. * Width for the timeline, a number in pixels or
  8527. * a css string like '1000px' or '75%'. '100%' by default.
  8528. * {String | Number} height
  8529. * Fixed height for the Timeline, a number in pixels or
  8530. * a css string like '400px' or '75%'. If undefined,
  8531. * The Timeline will automatically size such that
  8532. * its contents fit.
  8533. * {String | Number} minHeight
  8534. * Minimum height for the Timeline, a number in pixels or
  8535. * a css string like '400px' or '75%'.
  8536. * {String | Number} maxHeight
  8537. * Maximum height for the Timeline, a number in pixels or
  8538. * a css string like '400px' or '75%'.
  8539. * {Number | Date | String} start
  8540. * Start date for the visible window
  8541. * {Number | Date | String} end
  8542. * End date for the visible window
  8543. */
  8544. Timeline.prototype.setOptions = function (options) {
  8545. if (options) {
  8546. // copy the known options
  8547. var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
  8548. util.selectiveExtend(fields, this.options, options);
  8549. // enable/disable autoResize
  8550. this._initAutoResize();
  8551. }
  8552. // propagate options to all components
  8553. this.components.forEach(function (component) {
  8554. component.setOptions(options);
  8555. });
  8556. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  8557. if (options && options.order) {
  8558. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  8559. }
  8560. // redraw everything
  8561. this.redraw();
  8562. };
  8563. /**
  8564. * Set a custom time bar
  8565. * @param {Date} time
  8566. */
  8567. Timeline.prototype.setCustomTime = function (time) {
  8568. if (!this.customTime) {
  8569. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  8570. }
  8571. this.customTime.setCustomTime(time);
  8572. };
  8573. /**
  8574. * Retrieve the current custom time.
  8575. * @return {Date} customTime
  8576. */
  8577. Timeline.prototype.getCustomTime = function() {
  8578. if (!this.customTime) {
  8579. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  8580. }
  8581. return this.customTime.getCustomTime();
  8582. };
  8583. /**
  8584. * Set items
  8585. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  8586. */
  8587. Timeline.prototype.setItems = function(items) {
  8588. var initialLoad = (this.itemsData == null);
  8589. // convert to type DataSet when needed
  8590. var newDataSet;
  8591. if (!items) {
  8592. newDataSet = null;
  8593. }
  8594. else if (items instanceof DataSet || items instanceof DataView) {
  8595. newDataSet = items;
  8596. }
  8597. else {
  8598. // turn an array into a dataset
  8599. newDataSet = new DataSet(items, {
  8600. type: {
  8601. start: 'Date',
  8602. end: 'Date'
  8603. }
  8604. });
  8605. }
  8606. // set items
  8607. this.itemsData = newDataSet;
  8608. this.itemSet && this.itemSet.setItems(newDataSet);
  8609. if (initialLoad && ('start' in this.options || 'end' in this.options)) {
  8610. this.fit();
  8611. var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
  8612. var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
  8613. this.setWindow(start, end);
  8614. }
  8615. };
  8616. /**
  8617. * Set groups
  8618. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  8619. */
  8620. Timeline.prototype.setGroups = function(groups) {
  8621. // convert to type DataSet when needed
  8622. var newDataSet;
  8623. if (!groups) {
  8624. newDataSet = null;
  8625. }
  8626. else if (groups instanceof DataSet || groups instanceof DataView) {
  8627. newDataSet = groups;
  8628. }
  8629. else {
  8630. // turn an array into a dataset
  8631. newDataSet = new DataSet(groups);
  8632. }
  8633. this.groupsData = newDataSet;
  8634. this.itemSet.setGroups(newDataSet);
  8635. };
  8636. /**
  8637. * Clear the Timeline. By Default, items, groups and options are cleared.
  8638. * Example usage:
  8639. *
  8640. * timeline.clear(); // clear items, groups, and options
  8641. * timeline.clear({options: true}); // clear options only
  8642. *
  8643. * @param {Object} [what] Optionally specify what to clear. By default:
  8644. * {items: true, groups: true, options: true}
  8645. */
  8646. Timeline.prototype.clear = function(what) {
  8647. // clear items
  8648. if (!what || what.items) {
  8649. this.setItems(null);
  8650. }
  8651. // clear groups
  8652. if (!what || what.groups) {
  8653. this.setGroups(null);
  8654. }
  8655. // clear options of timeline and of each of the components
  8656. if (!what || what.options) {
  8657. this.components.forEach(function (component) {
  8658. component.setOptions(component.defaultOptions);
  8659. });
  8660. this.setOptions(this.defaultOptions); // this will also do a redraw
  8661. }
  8662. };
  8663. /**
  8664. * Set Timeline window such that it fits all items
  8665. */
  8666. Timeline.prototype.fit = function() {
  8667. // apply the data range as range
  8668. var dataRange = this.getItemRange();
  8669. // add 5% space on both sides
  8670. var start = dataRange.min;
  8671. var end = dataRange.max;
  8672. if (start != null && end != null) {
  8673. var interval = (end.valueOf() - start.valueOf());
  8674. if (interval <= 0) {
  8675. // prevent an empty interval
  8676. interval = 24 * 60 * 60 * 1000; // 1 day
  8677. }
  8678. start = new Date(start.valueOf() - interval * 0.05);
  8679. end = new Date(end.valueOf() + interval * 0.05);
  8680. }
  8681. // skip range set if there is no start and end date
  8682. if (start === null && end === null) {
  8683. return;
  8684. }
  8685. this.range.setRange(start, end);
  8686. };
  8687. /**
  8688. * Get the data range of the item set.
  8689. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  8690. * When no minimum is found, min==null
  8691. * When no maximum is found, max==null
  8692. */
  8693. Timeline.prototype.getItemRange = function() {
  8694. // calculate min from start filed
  8695. var dataset = this.itemsData.getDataSet(),
  8696. min = null,
  8697. max = null;
  8698. if (dataset) {
  8699. // calculate the minimum value of the field 'start'
  8700. var minItem = dataset.min('start');
  8701. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  8702. // Note: we convert first to Date and then to number because else
  8703. // a conversion from ISODate to Number will fail
  8704. // calculate maximum value of fields 'start' and 'end'
  8705. var maxStartItem = dataset.max('start');
  8706. if (maxStartItem) {
  8707. max = util.convert(maxStartItem.start, 'Date').valueOf();
  8708. }
  8709. var maxEndItem = dataset.max('end');
  8710. if (maxEndItem) {
  8711. if (max == null) {
  8712. max = util.convert(maxEndItem.end, 'Date').valueOf();
  8713. }
  8714. else {
  8715. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  8716. }
  8717. }
  8718. }
  8719. return {
  8720. min: (min != null) ? new Date(min) : null,
  8721. max: (max != null) ? new Date(max) : null
  8722. };
  8723. };
  8724. /**
  8725. * Set selected items by their id. Replaces the current selection
  8726. * Unknown id's are silently ignored.
  8727. * @param {Array} [ids] An array with zero or more id's of the items to be
  8728. * selected. If ids is an empty array, all items will be
  8729. * unselected.
  8730. */
  8731. Timeline.prototype.setSelection = function(ids) {
  8732. this.itemSet && this.itemSet.setSelection(ids);
  8733. };
  8734. /**
  8735. * Get the selected items by their id
  8736. * @return {Array} ids The ids of the selected items
  8737. */
  8738. Timeline.prototype.getSelection = function() {
  8739. return this.itemSet && this.itemSet.getSelection() || [];
  8740. };
  8741. /**
  8742. * Set the visible window. Both parameters are optional, you can change only
  8743. * start or only end. Syntax:
  8744. *
  8745. * TimeLine.setWindow(start, end)
  8746. * TimeLine.setWindow(range)
  8747. *
  8748. * Where start and end can be a Date, number, or string, and range is an
  8749. * object with properties start and end.
  8750. *
  8751. * @param {Date | Number | String | Object} [start] Start date of visible window
  8752. * @param {Date | Number | String} [end] End date of visible window
  8753. */
  8754. Timeline.prototype.setWindow = function(start, end) {
  8755. if (arguments.length == 1) {
  8756. var range = arguments[0];
  8757. this.range.setRange(range.start, range.end);
  8758. }
  8759. else {
  8760. this.range.setRange(start, end);
  8761. }
  8762. };
  8763. /**
  8764. * Get the visible window
  8765. * @return {{start: Date, end: Date}} Visible range
  8766. */
  8767. Timeline.prototype.getWindow = function() {
  8768. var range = this.range.getRange();
  8769. return {
  8770. start: new Date(range.start),
  8771. end: new Date(range.end)
  8772. };
  8773. };
  8774. /**
  8775. * Force a redraw of the Timeline. Can be useful to manually redraw when
  8776. * option autoResize=false
  8777. */
  8778. Timeline.prototype.redraw = function() {
  8779. var resized = false,
  8780. options = this.options,
  8781. props = this.props,
  8782. dom = this.dom;
  8783. if (!dom) return; // when destroyed
  8784. // update class names
  8785. dom.root.className = 'vis timeline root ' + options.orientation;
  8786. // update root width and height options
  8787. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  8788. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  8789. dom.root.style.width = util.option.asSize(options.width, '');
  8790. // calculate border widths
  8791. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  8792. props.border.right = props.border.left;
  8793. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  8794. props.border.bottom = props.border.top;
  8795. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  8796. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  8797. // calculate the heights. If any of the side panels is empty, we set the height to
  8798. // minus the border width, such that the border will be invisible
  8799. props.center.height = dom.center.offsetHeight;
  8800. props.left.height = dom.left.offsetHeight;
  8801. props.right.height = dom.right.offsetHeight;
  8802. props.top.height = dom.top.clientHeight || -props.border.top;
  8803. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  8804. // TODO: compensate borders when any of the panels is empty.
  8805. // apply auto height
  8806. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  8807. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  8808. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  8809. borderRootHeight + props.border.top + props.border.bottom;
  8810. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  8811. // calculate heights of the content panels
  8812. props.root.height = dom.root.offsetHeight;
  8813. props.background.height = props.root.height - borderRootHeight;
  8814. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  8815. borderRootHeight;
  8816. props.centerContainer.height = containerHeight;
  8817. props.leftContainer.height = containerHeight;
  8818. props.rightContainer.height = props.leftContainer.height;
  8819. // calculate the widths of the panels
  8820. props.root.width = dom.root.offsetWidth;
  8821. props.background.width = props.root.width - borderRootWidth;
  8822. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  8823. props.leftContainer.width = props.left.width;
  8824. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  8825. props.rightContainer.width = props.right.width;
  8826. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  8827. props.center.width = centerWidth;
  8828. props.centerContainer.width = centerWidth;
  8829. props.top.width = centerWidth;
  8830. props.bottom.width = centerWidth;
  8831. // resize the panels
  8832. dom.background.style.height = props.background.height + 'px';
  8833. dom.backgroundVertical.style.height = props.background.height + 'px';
  8834. dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
  8835. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  8836. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  8837. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  8838. dom.background.style.width = props.background.width + 'px';
  8839. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  8840. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  8841. dom.centerContainer.style.width = props.center.width + 'px';
  8842. dom.top.style.width = props.top.width + 'px';
  8843. dom.bottom.style.width = props.bottom.width + 'px';
  8844. // reposition the panels
  8845. dom.background.style.left = '0';
  8846. dom.background.style.top = '0';
  8847. dom.backgroundVertical.style.left = props.left.width + 'px';
  8848. dom.backgroundVertical.style.top = '0';
  8849. dom.backgroundHorizontal.style.left = '0';
  8850. dom.backgroundHorizontal.style.top = props.top.height + 'px';
  8851. dom.centerContainer.style.left = props.left.width + 'px';
  8852. dom.centerContainer.style.top = props.top.height + 'px';
  8853. dom.leftContainer.style.left = '0';
  8854. dom.leftContainer.style.top = props.top.height + 'px';
  8855. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  8856. dom.rightContainer.style.top = props.top.height + 'px';
  8857. dom.top.style.left = props.left.width + 'px';
  8858. dom.top.style.top = '0';
  8859. dom.bottom.style.left = props.left.width + 'px';
  8860. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  8861. // update the scrollTop, feasible range for the offset can be changed
  8862. // when the height of the Timeline or of the contents of the center changed
  8863. this._updateScrollTop();
  8864. // reposition the scrollable contents
  8865. var offset = this.props.scrollTop;
  8866. if (options.orientation == 'bottom') {
  8867. offset += Math.max(this.props.centerContainer.height - this.props.center.height -
  8868. this.props.border.top - this.props.border.bottom, 0);
  8869. }
  8870. dom.center.style.left = '0';
  8871. dom.center.style.top = offset + 'px';
  8872. dom.left.style.left = '0';
  8873. dom.left.style.top = offset + 'px';
  8874. dom.right.style.left = '0';
  8875. dom.right.style.top = offset + 'px';
  8876. // show shadows when vertical scrolling is available
  8877. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  8878. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  8879. dom.shadowTop.style.visibility = visibilityTop;
  8880. dom.shadowBottom.style.visibility = visibilityBottom;
  8881. dom.shadowTopLeft.style.visibility = visibilityTop;
  8882. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  8883. dom.shadowTopRight.style.visibility = visibilityTop;
  8884. dom.shadowBottomRight.style.visibility = visibilityBottom;
  8885. // redraw all components
  8886. this.components.forEach(function (component) {
  8887. resized = component.redraw() || resized;
  8888. });
  8889. if (resized) {
  8890. // keep repainting until all sizes are settled
  8891. this.redraw();
  8892. }
  8893. };
  8894. // TODO: deprecated since version 1.1.0, remove some day
  8895. Timeline.prototype.repaint = function () {
  8896. throw new Error('Function repaint is deprecated. Use redraw instead.');
  8897. };
  8898. /**
  8899. * Convert a position on screen (pixels) to a datetime
  8900. * @param {int} x Position on the screen in pixels
  8901. * @return {Date} time The datetime the corresponds with given position x
  8902. * @private
  8903. */
  8904. // TODO: move this function to Range
  8905. Timeline.prototype._toTime = function(x) {
  8906. var conversion = this.range.conversion(this.props.center.width);
  8907. return new Date(x / conversion.scale + conversion.offset);
  8908. };
  8909. /**
  8910. * Convert a position on the global screen (pixels) to a datetime
  8911. * @param {int} x Position on the screen in pixels
  8912. * @return {Date} time The datetime the corresponds with given position x
  8913. * @private
  8914. */
  8915. // TODO: move this function to Range
  8916. Timeline.prototype._toGlobalTime = function(x) {
  8917. var conversion = this.range.conversion(this.props.root.width);
  8918. return new Date(x / conversion.scale + conversion.offset);
  8919. };
  8920. /**
  8921. * Convert a datetime (Date object) into a position on the screen
  8922. * @param {Date} time A date
  8923. * @return {int} x The position on the screen in pixels which corresponds
  8924. * with the given date.
  8925. * @private
  8926. */
  8927. // TODO: move this function to Range
  8928. Timeline.prototype._toScreen = function(time) {
  8929. var conversion = this.range.conversion(this.props.center.width);
  8930. return (time.valueOf() - conversion.offset) * conversion.scale;
  8931. };
  8932. /**
  8933. * Convert a datetime (Date object) into a position on the root
  8934. * This is used to get the pixel density estimate for the screen, not the center panel
  8935. * @param {Date} time A date
  8936. * @return {int} x The position on root in pixels which corresponds
  8937. * with the given date.
  8938. * @private
  8939. */
  8940. // TODO: move this function to Range
  8941. Timeline.prototype._toGlobalScreen = function(time) {
  8942. var conversion = this.range.conversion(this.props.root.width);
  8943. return (time.valueOf() - conversion.offset) * conversion.scale;
  8944. };
  8945. /**
  8946. * Initialize watching when option autoResize is true
  8947. * @private
  8948. */
  8949. Timeline.prototype._initAutoResize = function () {
  8950. if (this.options.autoResize == true) {
  8951. this._startAutoResize();
  8952. }
  8953. else {
  8954. this._stopAutoResize();
  8955. }
  8956. };
  8957. /**
  8958. * Watch for changes in the size of the container. On resize, the Panel will
  8959. * automatically redraw itself.
  8960. * @private
  8961. */
  8962. Timeline.prototype._startAutoResize = function () {
  8963. var me = this;
  8964. this._stopAutoResize();
  8965. this._onResize = function() {
  8966. if (me.options.autoResize != true) {
  8967. // stop watching when the option autoResize is changed to false
  8968. me._stopAutoResize();
  8969. return;
  8970. }
  8971. if (me.dom.root) {
  8972. // check whether the frame is resized
  8973. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  8974. (me.dom.root.clientHeight != me.props.lastHeight)) {
  8975. me.props.lastWidth = me.dom.root.clientWidth;
  8976. me.props.lastHeight = me.dom.root.clientHeight;
  8977. me.emit('change');
  8978. }
  8979. }
  8980. };
  8981. // add event listener to window resize
  8982. util.addEventListener(window, 'resize', this._onResize);
  8983. this.watchTimer = setInterval(this._onResize, 1000);
  8984. };
  8985. /**
  8986. * Stop watching for a resize of the frame.
  8987. * @private
  8988. */
  8989. Timeline.prototype._stopAutoResize = function () {
  8990. if (this.watchTimer) {
  8991. clearInterval(this.watchTimer);
  8992. this.watchTimer = undefined;
  8993. }
  8994. // remove event listener on window.resize
  8995. util.removeEventListener(window, 'resize', this._onResize);
  8996. this._onResize = null;
  8997. };
  8998. /**
  8999. * Start moving the timeline vertically
  9000. * @param {Event} event
  9001. * @private
  9002. */
  9003. Timeline.prototype._onTouch = function (event) {
  9004. this.touch.allowDragging = true;
  9005. };
  9006. /**
  9007. * Start moving the timeline vertically
  9008. * @param {Event} event
  9009. * @private
  9010. */
  9011. Timeline.prototype._onPinch = function (event) {
  9012. this.touch.allowDragging = false;
  9013. };
  9014. /**
  9015. * Start moving the timeline vertically
  9016. * @param {Event} event
  9017. * @private
  9018. */
  9019. Timeline.prototype._onDragStart = function (event) {
  9020. this.touch.initialScrollTop = this.props.scrollTop;
  9021. };
  9022. /**
  9023. * Move the timeline vertically
  9024. * @param {Event} event
  9025. * @private
  9026. */
  9027. Timeline.prototype._onDrag = function (event) {
  9028. // refuse to drag when we where pinching to prevent the timeline make a jump
  9029. // when releasing the fingers in opposite order from the touch screen
  9030. if (!this.touch.allowDragging) return;
  9031. var delta = event.gesture.deltaY;
  9032. var oldScrollTop = this._getScrollTop();
  9033. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  9034. if (newScrollTop != oldScrollTop) {
  9035. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  9036. }
  9037. };
  9038. /**
  9039. * Apply a scrollTop
  9040. * @param {Number} scrollTop
  9041. * @returns {Number} scrollTop Returns the applied scrollTop
  9042. * @private
  9043. */
  9044. Timeline.prototype._setScrollTop = function (scrollTop) {
  9045. this.props.scrollTop = scrollTop;
  9046. this._updateScrollTop();
  9047. return this.props.scrollTop;
  9048. };
  9049. /**
  9050. * Update the current scrollTop when the height of the containers has been changed
  9051. * @returns {Number} scrollTop Returns the applied scrollTop
  9052. * @private
  9053. */
  9054. Timeline.prototype._updateScrollTop = function () {
  9055. // recalculate the scrollTopMin
  9056. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  9057. if (scrollTopMin != this.props.scrollTopMin) {
  9058. // in case of bottom orientation, change the scrollTop such that the contents
  9059. // do not move relative to the time axis at the bottom
  9060. if (this.options.orientation == 'bottom') {
  9061. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  9062. }
  9063. this.props.scrollTopMin = scrollTopMin;
  9064. }
  9065. // limit the scrollTop to the feasible scroll range
  9066. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  9067. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  9068. return this.props.scrollTop;
  9069. };
  9070. /**
  9071. * Get the current scrollTop
  9072. * @returns {number} scrollTop
  9073. * @private
  9074. */
  9075. Timeline.prototype._getScrollTop = function () {
  9076. return this.props.scrollTop;
  9077. };
  9078. /**
  9079. * Create a timeline visualization
  9080. * @param {HTMLElement} container
  9081. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  9082. * @param {Object} [options] See Graph2d.setOptions for the available options.
  9083. * @constructor
  9084. */
  9085. function Graph2d (container, items, options, groups) {
  9086. var me = this;
  9087. this.defaultOptions = {
  9088. start: null,
  9089. end: null,
  9090. autoResize: true,
  9091. orientation: 'bottom',
  9092. width: null,
  9093. height: null,
  9094. maxHeight: null,
  9095. minHeight: null
  9096. };
  9097. this.options = util.deepExtend({}, this.defaultOptions);
  9098. // Create the DOM, props, and emitter
  9099. this._create(container);
  9100. // all components listed here will be repainted automatically
  9101. this.components = [];
  9102. this.body = {
  9103. dom: this.dom,
  9104. domProps: this.props,
  9105. emitter: {
  9106. on: this.on.bind(this),
  9107. off: this.off.bind(this),
  9108. emit: this.emit.bind(this)
  9109. },
  9110. util: {
  9111. snap: null, // will be specified after TimeAxis is created
  9112. toScreen: me._toScreen.bind(me),
  9113. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  9114. toTime: me._toTime.bind(me),
  9115. toGlobalTime : me._toGlobalTime.bind(me)
  9116. }
  9117. };
  9118. // range
  9119. this.range = new Range(this.body);
  9120. this.components.push(this.range);
  9121. this.body.range = this.range;
  9122. // time axis
  9123. this.timeAxis = new TimeAxis(this.body);
  9124. this.components.push(this.timeAxis);
  9125. this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
  9126. // current time bar
  9127. this.currentTime = new CurrentTime(this.body);
  9128. this.components.push(this.currentTime);
  9129. // custom time bar
  9130. // Note: time bar will be attached in this.setOptions when selected
  9131. this.customTime = new CustomTime(this.body);
  9132. this.components.push(this.customTime);
  9133. // item set
  9134. this.linegraph = new LineGraph(this.body);
  9135. this.components.push(this.linegraph);
  9136. this.itemsData = null; // DataSet
  9137. this.groupsData = null; // DataSet
  9138. // apply options
  9139. if (options) {
  9140. this.setOptions(options);
  9141. }
  9142. // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
  9143. if (groups) {
  9144. this.setGroups(groups);
  9145. }
  9146. // create itemset
  9147. if (items) {
  9148. this.setItems(items);
  9149. }
  9150. else {
  9151. this.redraw();
  9152. }
  9153. }
  9154. // turn Graph2d into an event emitter
  9155. Emitter(Graph2d.prototype);
  9156. /**
  9157. * Create the main DOM for the Graph2d: a root panel containing left, right,
  9158. * top, bottom, content, and background panel.
  9159. * @param {Element} container The container element where the Graph2d will
  9160. * be attached.
  9161. * @private
  9162. */
  9163. Graph2d.prototype._create = function (container) {
  9164. this.dom = {};
  9165. this.dom.root = document.createElement('div');
  9166. this.dom.background = document.createElement('div');
  9167. this.dom.backgroundVertical = document.createElement('div');
  9168. this.dom.backgroundHorizontalContainer = document.createElement('div');
  9169. this.dom.centerContainer = document.createElement('div');
  9170. this.dom.leftContainer = document.createElement('div');
  9171. this.dom.rightContainer = document.createElement('div');
  9172. this.dom.backgroundHorizontal = document.createElement('div');
  9173. this.dom.center = document.createElement('div');
  9174. this.dom.left = document.createElement('div');
  9175. this.dom.right = document.createElement('div');
  9176. this.dom.top = document.createElement('div');
  9177. this.dom.bottom = document.createElement('div');
  9178. this.dom.shadowTop = document.createElement('div');
  9179. this.dom.shadowBottom = document.createElement('div');
  9180. this.dom.shadowTopLeft = document.createElement('div');
  9181. this.dom.shadowBottomLeft = document.createElement('div');
  9182. this.dom.shadowTopRight = document.createElement('div');
  9183. this.dom.shadowBottomRight = document.createElement('div');
  9184. this.dom.background.className = 'vispanel background';
  9185. this.dom.backgroundVertical.className = 'vispanel background vertical';
  9186. this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal';
  9187. this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
  9188. this.dom.centerContainer.className = 'vispanel center';
  9189. this.dom.leftContainer.className = 'vispanel left';
  9190. this.dom.rightContainer.className = 'vispanel right';
  9191. this.dom.top.className = 'vispanel top';
  9192. this.dom.bottom.className = 'vispanel bottom';
  9193. this.dom.left.className = 'content';
  9194. this.dom.center.className = 'content';
  9195. this.dom.right.className = 'content';
  9196. this.dom.shadowTop.className = 'shadow top';
  9197. this.dom.shadowBottom.className = 'shadow bottom';
  9198. this.dom.shadowTopLeft.className = 'shadow top';
  9199. this.dom.shadowBottomLeft.className = 'shadow bottom';
  9200. this.dom.shadowTopRight.className = 'shadow top';
  9201. this.dom.shadowBottomRight.className = 'shadow bottom';
  9202. this.dom.root.appendChild(this.dom.background);
  9203. this.dom.root.appendChild(this.dom.backgroundVertical);
  9204. this.dom.root.appendChild(this.dom.backgroundHorizontalContainer);
  9205. this.dom.root.appendChild(this.dom.centerContainer);
  9206. this.dom.root.appendChild(this.dom.leftContainer);
  9207. this.dom.root.appendChild(this.dom.rightContainer);
  9208. this.dom.root.appendChild(this.dom.top);
  9209. this.dom.root.appendChild(this.dom.bottom);
  9210. this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal);
  9211. this.dom.centerContainer.appendChild(this.dom.center);
  9212. this.dom.leftContainer.appendChild(this.dom.left);
  9213. this.dom.rightContainer.appendChild(this.dom.right);
  9214. this.dom.centerContainer.appendChild(this.dom.shadowTop);
  9215. this.dom.centerContainer.appendChild(this.dom.shadowBottom);
  9216. this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
  9217. this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
  9218. this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
  9219. this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
  9220. this.on('rangechange', this.redraw.bind(this));
  9221. this.on('change', this.redraw.bind(this));
  9222. this.on('touch', this._onTouch.bind(this));
  9223. this.on('pinch', this._onPinch.bind(this));
  9224. this.on('dragstart', this._onDragStart.bind(this));
  9225. this.on('drag', this._onDrag.bind(this));
  9226. // create event listeners for all interesting events, these events will be
  9227. // emitted via emitter
  9228. this.hammer = Hammer(this.dom.root, {
  9229. prevent_default: true
  9230. });
  9231. this.listeners = {};
  9232. var me = this;
  9233. var events = [
  9234. 'touch', 'pinch',
  9235. 'tap', 'doubletap', 'hold',
  9236. 'dragstart', 'drag', 'dragend',
  9237. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
  9238. ];
  9239. events.forEach(function (event) {
  9240. var listener = function () {
  9241. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  9242. me.emit.apply(me, args);
  9243. };
  9244. me.hammer.on(event, listener);
  9245. me.listeners[event] = listener;
  9246. });
  9247. // size properties of each of the panels
  9248. this.props = {
  9249. root: {},
  9250. background: {},
  9251. centerContainer: {},
  9252. leftContainer: {},
  9253. rightContainer: {},
  9254. center: {},
  9255. left: {},
  9256. right: {},
  9257. top: {},
  9258. bottom: {},
  9259. border: {},
  9260. scrollTop: 0,
  9261. scrollTopMin: 0
  9262. };
  9263. this.touch = {}; // store state information needed for touch events
  9264. // attach the root panel to the provided container
  9265. if (!container) throw new Error('No container provided');
  9266. container.appendChild(this.dom.root);
  9267. };
  9268. /**
  9269. * Destroy the Graph2d, clean up all DOM elements and event listeners.
  9270. */
  9271. Graph2d.prototype.destroy = function () {
  9272. // unbind datasets
  9273. this.clear();
  9274. // remove all event listeners
  9275. this.off();
  9276. // stop checking for changed size
  9277. this._stopAutoResize();
  9278. // remove from DOM
  9279. if (this.dom.root.parentNode) {
  9280. this.dom.root.parentNode.removeChild(this.dom.root);
  9281. }
  9282. this.dom = null;
  9283. // cleanup hammer touch events
  9284. for (var event in this.listeners) {
  9285. if (this.listeners.hasOwnProperty(event)) {
  9286. delete this.listeners[event];
  9287. }
  9288. }
  9289. this.listeners = null;
  9290. this.hammer = null;
  9291. // give all components the opportunity to cleanup
  9292. this.components.forEach(function (component) {
  9293. component.destroy();
  9294. });
  9295. this.body = null;
  9296. };
  9297. /**
  9298. * Set options. Options will be passed to all components loaded in the Graph2d.
  9299. * @param {Object} [options]
  9300. * {String} orientation
  9301. * Vertical orientation for the Graph2d,
  9302. * can be 'bottom' (default) or 'top'.
  9303. * {String | Number} width
  9304. * Width for the timeline, a number in pixels or
  9305. * a css string like '1000px' or '75%'. '100%' by default.
  9306. * {String | Number} height
  9307. * Fixed height for the Graph2d, a number in pixels or
  9308. * a css string like '400px' or '75%'. If undefined,
  9309. * The Graph2d will automatically size such that
  9310. * its contents fit.
  9311. * {String | Number} minHeight
  9312. * Minimum height for the Graph2d, a number in pixels or
  9313. * a css string like '400px' or '75%'.
  9314. * {String | Number} maxHeight
  9315. * Maximum height for the Graph2d, a number in pixels or
  9316. * a css string like '400px' or '75%'.
  9317. * {Number | Date | String} start
  9318. * Start date for the visible window
  9319. * {Number | Date | String} end
  9320. * End date for the visible window
  9321. */
  9322. Graph2d.prototype.setOptions = function (options) {
  9323. if (options) {
  9324. // copy the known options
  9325. var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
  9326. util.selectiveExtend(fields, this.options, options);
  9327. // enable/disable autoResize
  9328. this._initAutoResize();
  9329. }
  9330. // propagate options to all components
  9331. this.components.forEach(function (component) {
  9332. component.setOptions(options);
  9333. });
  9334. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  9335. if (options && options.order) {
  9336. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  9337. }
  9338. // redraw everything
  9339. this.redraw();
  9340. };
  9341. /**
  9342. * Set a custom time bar
  9343. * @param {Date} time
  9344. */
  9345. Graph2d.prototype.setCustomTime = function (time) {
  9346. if (!this.customTime) {
  9347. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  9348. }
  9349. this.customTime.setCustomTime(time);
  9350. };
  9351. /**
  9352. * Retrieve the current custom time.
  9353. * @return {Date} customTime
  9354. */
  9355. Graph2d.prototype.getCustomTime = function() {
  9356. if (!this.customTime) {
  9357. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  9358. }
  9359. return this.customTime.getCustomTime();
  9360. };
  9361. /**
  9362. * Set items
  9363. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  9364. */
  9365. Graph2d.prototype.setItems = function(items) {
  9366. var initialLoad = (this.itemsData == null);
  9367. // convert to type DataSet when needed
  9368. var newDataSet;
  9369. if (!items) {
  9370. newDataSet = null;
  9371. }
  9372. else if (items instanceof DataSet || items instanceof DataView) {
  9373. newDataSet = items;
  9374. }
  9375. else {
  9376. // turn an array into a dataset
  9377. newDataSet = new DataSet(items, {
  9378. type: {
  9379. start: 'Date',
  9380. end: 'Date'
  9381. }
  9382. });
  9383. }
  9384. // set items
  9385. this.itemsData = newDataSet;
  9386. this.linegraph && this.linegraph.setItems(newDataSet);
  9387. if (initialLoad && ('start' in this.options || 'end' in this.options)) {
  9388. this.fit();
  9389. var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
  9390. var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
  9391. this.setWindow(start, end);
  9392. }
  9393. };
  9394. /**
  9395. * Set groups
  9396. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  9397. */
  9398. Graph2d.prototype.setGroups = function(groups) {
  9399. // convert to type DataSet when needed
  9400. var newDataSet;
  9401. if (!groups) {
  9402. newDataSet = null;
  9403. }
  9404. else if (groups instanceof DataSet || groups instanceof DataView) {
  9405. newDataSet = groups;
  9406. }
  9407. else {
  9408. // turn an array into a dataset
  9409. newDataSet = new DataSet(groups);
  9410. }
  9411. this.groupsData = newDataSet;
  9412. this.linegraph.setGroups(newDataSet);
  9413. };
  9414. /**
  9415. * Clear the Graph2d. By Default, items, groups and options are cleared.
  9416. * Example usage:
  9417. *
  9418. * timeline.clear(); // clear items, groups, and options
  9419. * timeline.clear({options: true}); // clear options only
  9420. *
  9421. * @param {Object} [what] Optionally specify what to clear. By default:
  9422. * {items: true, groups: true, options: true}
  9423. */
  9424. Graph2d.prototype.clear = function(what) {
  9425. // clear items
  9426. if (!what || what.items) {
  9427. this.setItems(null);
  9428. }
  9429. // clear groups
  9430. if (!what || what.groups) {
  9431. this.setGroups(null);
  9432. }
  9433. // clear options of timeline and of each of the components
  9434. if (!what || what.options) {
  9435. this.components.forEach(function (component) {
  9436. component.setOptions(component.defaultOptions);
  9437. });
  9438. this.setOptions(this.defaultOptions); // this will also do a redraw
  9439. }
  9440. };
  9441. /**
  9442. * Set Graph2d window such that it fits all items
  9443. */
  9444. Graph2d.prototype.fit = function() {
  9445. // apply the data range as range
  9446. var dataRange = this.getItemRange();
  9447. // add 5% space on both sides
  9448. var start = dataRange.min;
  9449. var end = dataRange.max;
  9450. if (start != null && end != null) {
  9451. var interval = (end.valueOf() - start.valueOf());
  9452. if (interval <= 0) {
  9453. // prevent an empty interval
  9454. interval = 24 * 60 * 60 * 1000; // 1 day
  9455. }
  9456. start = new Date(start.valueOf() - interval * 0.05);
  9457. end = new Date(end.valueOf() + interval * 0.05);
  9458. }
  9459. // skip range set if there is no start and end date
  9460. if (start === null && end === null) {
  9461. return;
  9462. }
  9463. this.range.setRange(start, end);
  9464. };
  9465. /**
  9466. * Get the data range of the item set.
  9467. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  9468. * When no minimum is found, min==null
  9469. * When no maximum is found, max==null
  9470. */
  9471. Graph2d.prototype.getItemRange = function() {
  9472. // calculate min from start filed
  9473. var itemsData = this.itemsData,
  9474. min = null,
  9475. max = null;
  9476. if (itemsData) {
  9477. // calculate the minimum value of the field 'start'
  9478. var minItem = itemsData.min('start');
  9479. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  9480. // Note: we convert first to Date and then to number because else
  9481. // a conversion from ISODate to Number will fail
  9482. // calculate maximum value of fields 'start' and 'end'
  9483. var maxStartItem = itemsData.max('start');
  9484. if (maxStartItem) {
  9485. max = util.convert(maxStartItem.start, 'Date').valueOf();
  9486. }
  9487. var maxEndItem = itemsData.max('end');
  9488. if (maxEndItem) {
  9489. if (max == null) {
  9490. max = util.convert(maxEndItem.end, 'Date').valueOf();
  9491. }
  9492. else {
  9493. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  9494. }
  9495. }
  9496. }
  9497. return {
  9498. min: (min != null) ? new Date(min) : null,
  9499. max: (max != null) ? new Date(max) : null
  9500. };
  9501. };
  9502. /**
  9503. * Set the visible window. Both parameters are optional, you can change only
  9504. * start or only end. Syntax:
  9505. *
  9506. * TimeLine.setWindow(start, end)
  9507. * TimeLine.setWindow(range)
  9508. *
  9509. * Where start and end can be a Date, number, or string, and range is an
  9510. * object with properties start and end.
  9511. *
  9512. * @param {Date | Number | String | Object} [start] Start date of visible window
  9513. * @param {Date | Number | String} [end] End date of visible window
  9514. */
  9515. Graph2d.prototype.setWindow = function(start, end) {
  9516. if (arguments.length == 1) {
  9517. var range = arguments[0];
  9518. this.range.setRange(range.start, range.end);
  9519. }
  9520. else {
  9521. this.range.setRange(start, end);
  9522. }
  9523. };
  9524. /**
  9525. * Get the visible window
  9526. * @return {{start: Date, end: Date}} Visible range
  9527. */
  9528. Graph2d.prototype.getWindow = function() {
  9529. var range = this.range.getRange();
  9530. return {
  9531. start: new Date(range.start),
  9532. end: new Date(range.end)
  9533. };
  9534. };
  9535. /**
  9536. * Force a redraw of the Graph2d. Can be useful to manually redraw when
  9537. * option autoResize=false
  9538. */
  9539. Graph2d.prototype.redraw = function() {
  9540. var resized = false,
  9541. options = this.options,
  9542. props = this.props,
  9543. dom = this.dom;
  9544. if (!dom) return; // when destroyed
  9545. // update class names
  9546. dom.root.className = 'vis timeline root ' + options.orientation;
  9547. // update root width and height options
  9548. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  9549. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  9550. dom.root.style.width = util.option.asSize(options.width, '');
  9551. // calculate border widths
  9552. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  9553. props.border.right = props.border.left;
  9554. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  9555. props.border.bottom = props.border.top;
  9556. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  9557. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  9558. // calculate the heights. If any of the side panels is empty, we set the height to
  9559. // minus the border width, such that the border will be invisible
  9560. props.center.height = dom.center.offsetHeight;
  9561. props.left.height = dom.left.offsetHeight;
  9562. props.right.height = dom.right.offsetHeight;
  9563. props.top.height = dom.top.clientHeight || -props.border.top;
  9564. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  9565. // TODO: compensate borders when any of the panels is empty.
  9566. // apply auto height
  9567. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  9568. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  9569. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  9570. borderRootHeight + props.border.top + props.border.bottom;
  9571. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  9572. // calculate heights of the content panels
  9573. props.root.height = dom.root.offsetHeight;
  9574. props.background.height = props.root.height - borderRootHeight;
  9575. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  9576. borderRootHeight;
  9577. props.centerContainer.height = containerHeight;
  9578. props.leftContainer.height = containerHeight;
  9579. props.rightContainer.height = props.leftContainer.height;
  9580. // calculate the widths of the panels
  9581. props.root.width = dom.root.offsetWidth;
  9582. props.background.width = props.root.width - borderRootWidth;
  9583. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  9584. props.leftContainer.width = props.left.width;
  9585. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  9586. props.rightContainer.width = props.right.width;
  9587. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  9588. props.center.width = centerWidth;
  9589. props.centerContainer.width = centerWidth;
  9590. props.top.width = centerWidth;
  9591. props.bottom.width = centerWidth;
  9592. // resize the panels
  9593. dom.background.style.height = props.background.height + 'px';
  9594. dom.backgroundVertical.style.height = props.background.height + 'px';
  9595. dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px';
  9596. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  9597. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  9598. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  9599. dom.background.style.width = props.background.width + 'px';
  9600. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  9601. dom.backgroundHorizontalContainer.style.width = props.background.width + 'px';
  9602. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  9603. dom.centerContainer.style.width = props.center.width + 'px';
  9604. dom.top.style.width = props.top.width + 'px';
  9605. dom.bottom.style.width = props.bottom.width + 'px';
  9606. // reposition the panels
  9607. dom.background.style.left = '0';
  9608. dom.background.style.top = '0';
  9609. dom.backgroundVertical.style.left = props.left.width + 'px';
  9610. dom.backgroundVertical.style.top = '0';
  9611. dom.backgroundHorizontalContainer.style.left = '0';
  9612. dom.backgroundHorizontalContainer.style.top = props.top.height + 'px';
  9613. dom.centerContainer.style.left = props.left.width + 'px';
  9614. dom.centerContainer.style.top = props.top.height + 'px';
  9615. dom.leftContainer.style.left = '0';
  9616. dom.leftContainer.style.top = props.top.height + 'px';
  9617. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  9618. dom.rightContainer.style.top = props.top.height + 'px';
  9619. dom.top.style.left = props.left.width + 'px';
  9620. dom.top.style.top = '0';
  9621. dom.bottom.style.left = props.left.width + 'px';
  9622. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  9623. // update the scrollTop, feasible range for the offset can be changed
  9624. // when the height of the Graph2d or of the contents of the center changed
  9625. this._updateScrollTop();
  9626. // reposition the scrollable contents
  9627. var offset = this.props.scrollTop;
  9628. if (options.orientation == 'bottom') {
  9629. offset += Math.max(this.props.centerContainer.height - this.props.center.height -
  9630. this.props.border.top - this.props.border.bottom, 0);
  9631. }
  9632. dom.center.style.left = '0';
  9633. dom.center.style.top = offset + 'px';
  9634. dom.backgroundHorizontal.style.left = '0';
  9635. dom.backgroundHorizontal.style.top = offset + 'px';
  9636. dom.left.style.left = '0';
  9637. dom.left.style.top = offset + 'px';
  9638. dom.right.style.left = '0';
  9639. dom.right.style.top = offset + 'px';
  9640. // show shadows when vertical scrolling is available
  9641. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  9642. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  9643. dom.shadowTop.style.visibility = visibilityTop;
  9644. dom.shadowBottom.style.visibility = visibilityBottom;
  9645. dom.shadowTopLeft.style.visibility = visibilityTop;
  9646. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  9647. dom.shadowTopRight.style.visibility = visibilityTop;
  9648. dom.shadowBottomRight.style.visibility = visibilityBottom;
  9649. // redraw all components
  9650. this.components.forEach(function (component) {
  9651. resized = component.redraw() || resized;
  9652. });
  9653. if (resized) {
  9654. // keep redrawing until all sizes are settled
  9655. this.redraw();
  9656. }
  9657. };
  9658. /**
  9659. * Convert a position on screen (pixels) to a datetime
  9660. * @param {int} x Position on the screen in pixels
  9661. * @return {Date} time The datetime the corresponds with given position x
  9662. * @private
  9663. */
  9664. // TODO: move this function to Range
  9665. Graph2d.prototype._toTime = function(x) {
  9666. var conversion = this.range.conversion(this.props.center.width);
  9667. return new Date(x / conversion.scale + conversion.offset);
  9668. };
  9669. /**
  9670. * Convert a datetime (Date object) into a position on the root
  9671. * This is used to get the pixel density estimate for the screen, not the center panel
  9672. * @param {Date} time A date
  9673. * @return {int} x The position on root in pixels which corresponds
  9674. * with the given date.
  9675. * @private
  9676. */
  9677. // TODO: move this function to Range
  9678. Graph2d.prototype._toGlobalTime = function(x) {
  9679. var conversion = this.range.conversion(this.props.root.width);
  9680. return new Date(x / conversion.scale + conversion.offset);
  9681. };
  9682. /**
  9683. * Convert a datetime (Date object) into a position on the screen
  9684. * @param {Date} time A date
  9685. * @return {int} x The position on the screen in pixels which corresponds
  9686. * with the given date.
  9687. * @private
  9688. */
  9689. // TODO: move this function to Range
  9690. Graph2d.prototype._toScreen = function(time) {
  9691. var conversion = this.range.conversion(this.props.center.width);
  9692. return (time.valueOf() - conversion.offset) * conversion.scale;
  9693. };
  9694. /**
  9695. * Convert a datetime (Date object) into a position on the root
  9696. * This is used to get the pixel density estimate for the screen, not the center panel
  9697. * @param {Date} time A date
  9698. * @return {int} x The position on root in pixels which corresponds
  9699. * with the given date.
  9700. * @private
  9701. */
  9702. // TODO: move this function to Range
  9703. Graph2d.prototype._toGlobalScreen = function(time) {
  9704. var conversion = this.range.conversion(this.props.root.width);
  9705. return (time.valueOf() - conversion.offset) * conversion.scale;
  9706. };
  9707. /**
  9708. * Initialize watching when option autoResize is true
  9709. * @private
  9710. */
  9711. Graph2d.prototype._initAutoResize = function () {
  9712. if (this.options.autoResize == true) {
  9713. this._startAutoResize();
  9714. }
  9715. else {
  9716. this._stopAutoResize();
  9717. }
  9718. };
  9719. /**
  9720. * Watch for changes in the size of the container. On resize, the Panel will
  9721. * automatically redraw itself.
  9722. * @private
  9723. */
  9724. Graph2d.prototype._startAutoResize = function () {
  9725. var me = this;
  9726. this._stopAutoResize();
  9727. this._onResize = function() {
  9728. if (me.options.autoResize != true) {
  9729. // stop watching when the option autoResize is changed to false
  9730. me._stopAutoResize();
  9731. return;
  9732. }
  9733. if (me.dom.root) {
  9734. // check whether the frame is resized
  9735. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  9736. (me.dom.root.clientHeight != me.props.lastHeight)) {
  9737. me.props.lastWidth = me.dom.root.clientWidth;
  9738. me.props.lastHeight = me.dom.root.clientHeight;
  9739. me.emit('change');
  9740. }
  9741. }
  9742. };
  9743. // add event listener to window resize
  9744. util.addEventListener(window, 'resize', this._onResize);
  9745. this.watchTimer = setInterval(this._onResize, 1000);
  9746. };
  9747. /**
  9748. * Stop watching for a resize of the frame.
  9749. * @private
  9750. */
  9751. Graph2d.prototype._stopAutoResize = function () {
  9752. if (this.watchTimer) {
  9753. clearInterval(this.watchTimer);
  9754. this.watchTimer = undefined;
  9755. }
  9756. // remove event listener on window.resize
  9757. util.removeEventListener(window, 'resize', this._onResize);
  9758. this._onResize = null;
  9759. };
  9760. /**
  9761. * Start moving the timeline vertically
  9762. * @param {Event} event
  9763. * @private
  9764. */
  9765. Graph2d.prototype._onTouch = function (event) {
  9766. this.touch.allowDragging = true;
  9767. };
  9768. /**
  9769. * Start moving the timeline vertically
  9770. * @param {Event} event
  9771. * @private
  9772. */
  9773. Graph2d.prototype._onPinch = function (event) {
  9774. this.touch.allowDragging = false;
  9775. };
  9776. /**
  9777. * Start moving the timeline vertically
  9778. * @param {Event} event
  9779. * @private
  9780. */
  9781. Graph2d.prototype._onDragStart = function (event) {
  9782. this.touch.initialScrollTop = this.props.scrollTop;
  9783. };
  9784. /**
  9785. * Move the timeline vertically
  9786. * @param {Event} event
  9787. * @private
  9788. */
  9789. Graph2d.prototype._onDrag = function (event) {
  9790. // refuse to drag when we where pinching to prevent the timeline make a jump
  9791. // when releasing the fingers in opposite order from the touch screen
  9792. if (!this.touch.allowDragging) return;
  9793. var delta = event.gesture.deltaY;
  9794. var oldScrollTop = this._getScrollTop();
  9795. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  9796. if (newScrollTop != oldScrollTop) {
  9797. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  9798. }
  9799. };
  9800. /**
  9801. * Apply a scrollTop
  9802. * @param {Number} scrollTop
  9803. * @returns {Number} scrollTop Returns the applied scrollTop
  9804. * @private
  9805. */
  9806. Graph2d.prototype._setScrollTop = function (scrollTop) {
  9807. this.props.scrollTop = scrollTop;
  9808. this._updateScrollTop();
  9809. return this.props.scrollTop;
  9810. };
  9811. /**
  9812. * Update the current scrollTop when the height of the containers has been changed
  9813. * @returns {Number} scrollTop Returns the applied scrollTop
  9814. * @private
  9815. */
  9816. Graph2d.prototype._updateScrollTop = function () {
  9817. // recalculate the scrollTopMin
  9818. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  9819. if (scrollTopMin != this.props.scrollTopMin) {
  9820. // in case of bottom orientation, change the scrollTop such that the contents
  9821. // do not move relative to the time axis at the bottom
  9822. if (this.options.orientation == 'bottom') {
  9823. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  9824. }
  9825. this.props.scrollTopMin = scrollTopMin;
  9826. }
  9827. // limit the scrollTop to the feasible scroll range
  9828. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  9829. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  9830. return this.props.scrollTop;
  9831. };
  9832. /**
  9833. * Get the current scrollTop
  9834. * @returns {number} scrollTop
  9835. * @private
  9836. */
  9837. Graph2d.prototype._getScrollTop = function () {
  9838. return this.props.scrollTop;
  9839. };
  9840. (function(exports) {
  9841. /**
  9842. * Parse a text source containing data in DOT language into a JSON object.
  9843. * The object contains two lists: one with nodes and one with edges.
  9844. *
  9845. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  9846. *
  9847. * @param {String} data Text containing a graph in DOT-notation
  9848. * @return {Object} graph An object containing two parameters:
  9849. * {Object[]} nodes
  9850. * {Object[]} edges
  9851. */
  9852. function parseDOT (data) {
  9853. dot = data;
  9854. return parseGraph();
  9855. }
  9856. // token types enumeration
  9857. var TOKENTYPE = {
  9858. NULL : 0,
  9859. DELIMITER : 1,
  9860. IDENTIFIER: 2,
  9861. UNKNOWN : 3
  9862. };
  9863. // map with all delimiters
  9864. var DELIMITERS = {
  9865. '{': true,
  9866. '}': true,
  9867. '[': true,
  9868. ']': true,
  9869. ';': true,
  9870. '=': true,
  9871. ',': true,
  9872. '->': true,
  9873. '--': true
  9874. };
  9875. var dot = ''; // current dot file
  9876. var index = 0; // current index in dot file
  9877. var c = ''; // current token character in expr
  9878. var token = ''; // current token
  9879. var tokenType = TOKENTYPE.NULL; // type of the token
  9880. /**
  9881. * Get the first character from the dot file.
  9882. * The character is stored into the char c. If the end of the dot file is
  9883. * reached, the function puts an empty string in c.
  9884. */
  9885. function first() {
  9886. index = 0;
  9887. c = dot.charAt(0);
  9888. }
  9889. /**
  9890. * Get the next character from the dot file.
  9891. * The character is stored into the char c. If the end of the dot file is
  9892. * reached, the function puts an empty string in c.
  9893. */
  9894. function next() {
  9895. index++;
  9896. c = dot.charAt(index);
  9897. }
  9898. /**
  9899. * Preview the next character from the dot file.
  9900. * @return {String} cNext
  9901. */
  9902. function nextPreview() {
  9903. return dot.charAt(index + 1);
  9904. }
  9905. /**
  9906. * Test whether given character is alphabetic or numeric
  9907. * @param {String} c
  9908. * @return {Boolean} isAlphaNumeric
  9909. */
  9910. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  9911. function isAlphaNumeric(c) {
  9912. return regexAlphaNumeric.test(c);
  9913. }
  9914. /**
  9915. * Merge all properties of object b into object b
  9916. * @param {Object} a
  9917. * @param {Object} b
  9918. * @return {Object} a
  9919. */
  9920. function merge (a, b) {
  9921. if (!a) {
  9922. a = {};
  9923. }
  9924. if (b) {
  9925. for (var name in b) {
  9926. if (b.hasOwnProperty(name)) {
  9927. a[name] = b[name];
  9928. }
  9929. }
  9930. }
  9931. return a;
  9932. }
  9933. /**
  9934. * Set a value in an object, where the provided parameter name can be a
  9935. * path with nested parameters. For example:
  9936. *
  9937. * var obj = {a: 2};
  9938. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  9939. *
  9940. * @param {Object} obj
  9941. * @param {String} path A parameter name or dot-separated parameter path,
  9942. * like "color.highlight.border".
  9943. * @param {*} value
  9944. */
  9945. function setValue(obj, path, value) {
  9946. var keys = path.split('.');
  9947. var o = obj;
  9948. while (keys.length) {
  9949. var key = keys.shift();
  9950. if (keys.length) {
  9951. // this isn't the end point
  9952. if (!o[key]) {
  9953. o[key] = {};
  9954. }
  9955. o = o[key];
  9956. }
  9957. else {
  9958. // this is the end point
  9959. o[key] = value;
  9960. }
  9961. }
  9962. }
  9963. /**
  9964. * Add a node to a graph object. If there is already a node with
  9965. * the same id, their attributes will be merged.
  9966. * @param {Object} graph
  9967. * @param {Object} node
  9968. */
  9969. function addNode(graph, node) {
  9970. var i, len;
  9971. var current = null;
  9972. // find root graph (in case of subgraph)
  9973. var graphs = [graph]; // list with all graphs from current graph to root graph
  9974. var root = graph;
  9975. while (root.parent) {
  9976. graphs.push(root.parent);
  9977. root = root.parent;
  9978. }
  9979. // find existing node (at root level) by its id
  9980. if (root.nodes) {
  9981. for (i = 0, len = root.nodes.length; i < len; i++) {
  9982. if (node.id === root.nodes[i].id) {
  9983. current = root.nodes[i];
  9984. break;
  9985. }
  9986. }
  9987. }
  9988. if (!current) {
  9989. // this is a new node
  9990. current = {
  9991. id: node.id
  9992. };
  9993. if (graph.node) {
  9994. // clone default attributes
  9995. current.attr = merge(current.attr, graph.node);
  9996. }
  9997. }
  9998. // add node to this (sub)graph and all its parent graphs
  9999. for (i = graphs.length - 1; i >= 0; i--) {
  10000. var g = graphs[i];
  10001. if (!g.nodes) {
  10002. g.nodes = [];
  10003. }
  10004. if (g.nodes.indexOf(current) == -1) {
  10005. g.nodes.push(current);
  10006. }
  10007. }
  10008. // merge attributes
  10009. if (node.attr) {
  10010. current.attr = merge(current.attr, node.attr);
  10011. }
  10012. }
  10013. /**
  10014. * Add an edge to a graph object
  10015. * @param {Object} graph
  10016. * @param {Object} edge
  10017. */
  10018. function addEdge(graph, edge) {
  10019. if (!graph.edges) {
  10020. graph.edges = [];
  10021. }
  10022. graph.edges.push(edge);
  10023. if (graph.edge) {
  10024. var attr = merge({}, graph.edge); // clone default attributes
  10025. edge.attr = merge(attr, edge.attr); // merge attributes
  10026. }
  10027. }
  10028. /**
  10029. * Create an edge to a graph object
  10030. * @param {Object} graph
  10031. * @param {String | Number | Object} from
  10032. * @param {String | Number | Object} to
  10033. * @param {String} type
  10034. * @param {Object | null} attr
  10035. * @return {Object} edge
  10036. */
  10037. function createEdge(graph, from, to, type, attr) {
  10038. var edge = {
  10039. from: from,
  10040. to: to,
  10041. type: type
  10042. };
  10043. if (graph.edge) {
  10044. edge.attr = merge({}, graph.edge); // clone default attributes
  10045. }
  10046. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  10047. return edge;
  10048. }
  10049. /**
  10050. * Get next token in the current dot file.
  10051. * The token and token type are available as token and tokenType
  10052. */
  10053. function getToken() {
  10054. tokenType = TOKENTYPE.NULL;
  10055. token = '';
  10056. // skip over whitespaces
  10057. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10058. next();
  10059. }
  10060. do {
  10061. var isComment = false;
  10062. // skip comment
  10063. if (c == '#') {
  10064. // find the previous non-space character
  10065. var i = index - 1;
  10066. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  10067. i--;
  10068. }
  10069. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  10070. // the # is at the start of a line, this is indeed a line comment
  10071. while (c != '' && c != '\n') {
  10072. next();
  10073. }
  10074. isComment = true;
  10075. }
  10076. }
  10077. if (c == '/' && nextPreview() == '/') {
  10078. // skip line comment
  10079. while (c != '' && c != '\n') {
  10080. next();
  10081. }
  10082. isComment = true;
  10083. }
  10084. if (c == '/' && nextPreview() == '*') {
  10085. // skip block comment
  10086. while (c != '') {
  10087. if (c == '*' && nextPreview() == '/') {
  10088. // end of block comment found. skip these last two characters
  10089. next();
  10090. next();
  10091. break;
  10092. }
  10093. else {
  10094. next();
  10095. }
  10096. }
  10097. isComment = true;
  10098. }
  10099. // skip over whitespaces
  10100. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10101. next();
  10102. }
  10103. }
  10104. while (isComment);
  10105. // check for end of dot file
  10106. if (c == '') {
  10107. // token is still empty
  10108. tokenType = TOKENTYPE.DELIMITER;
  10109. return;
  10110. }
  10111. // check for delimiters consisting of 2 characters
  10112. var c2 = c + nextPreview();
  10113. if (DELIMITERS[c2]) {
  10114. tokenType = TOKENTYPE.DELIMITER;
  10115. token = c2;
  10116. next();
  10117. next();
  10118. return;
  10119. }
  10120. // check for delimiters consisting of 1 character
  10121. if (DELIMITERS[c]) {
  10122. tokenType = TOKENTYPE.DELIMITER;
  10123. token = c;
  10124. next();
  10125. return;
  10126. }
  10127. // check for an identifier (number or string)
  10128. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  10129. if (isAlphaNumeric(c) || c == '-') {
  10130. token += c;
  10131. next();
  10132. while (isAlphaNumeric(c)) {
  10133. token += c;
  10134. next();
  10135. }
  10136. if (token == 'false') {
  10137. token = false; // convert to boolean
  10138. }
  10139. else if (token == 'true') {
  10140. token = true; // convert to boolean
  10141. }
  10142. else if (!isNaN(Number(token))) {
  10143. token = Number(token); // convert to number
  10144. }
  10145. tokenType = TOKENTYPE.IDENTIFIER;
  10146. return;
  10147. }
  10148. // check for a string enclosed by double quotes
  10149. if (c == '"') {
  10150. next();
  10151. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  10152. token += c;
  10153. if (c == '"') { // skip the escape character
  10154. next();
  10155. }
  10156. next();
  10157. }
  10158. if (c != '"') {
  10159. throw newSyntaxError('End of string " expected');
  10160. }
  10161. next();
  10162. tokenType = TOKENTYPE.IDENTIFIER;
  10163. return;
  10164. }
  10165. // something unknown is found, wrong characters, a syntax error
  10166. tokenType = TOKENTYPE.UNKNOWN;
  10167. while (c != '') {
  10168. token += c;
  10169. next();
  10170. }
  10171. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  10172. }
  10173. /**
  10174. * Parse a graph.
  10175. * @returns {Object} graph
  10176. */
  10177. function parseGraph() {
  10178. var graph = {};
  10179. first();
  10180. getToken();
  10181. // optional strict keyword
  10182. if (token == 'strict') {
  10183. graph.strict = true;
  10184. getToken();
  10185. }
  10186. // graph or digraph keyword
  10187. if (token == 'graph' || token == 'digraph') {
  10188. graph.type = token;
  10189. getToken();
  10190. }
  10191. // optional graph id
  10192. if (tokenType == TOKENTYPE.IDENTIFIER) {
  10193. graph.id = token;
  10194. getToken();
  10195. }
  10196. // open angle bracket
  10197. if (token != '{') {
  10198. throw newSyntaxError('Angle bracket { expected');
  10199. }
  10200. getToken();
  10201. // statements
  10202. parseStatements(graph);
  10203. // close angle bracket
  10204. if (token != '}') {
  10205. throw newSyntaxError('Angle bracket } expected');
  10206. }
  10207. getToken();
  10208. // end of file
  10209. if (token !== '') {
  10210. throw newSyntaxError('End of file expected');
  10211. }
  10212. getToken();
  10213. // remove temporary default properties
  10214. delete graph.node;
  10215. delete graph.edge;
  10216. delete graph.graph;
  10217. return graph;
  10218. }
  10219. /**
  10220. * Parse a list with statements.
  10221. * @param {Object} graph
  10222. */
  10223. function parseStatements (graph) {
  10224. while (token !== '' && token != '}') {
  10225. parseStatement(graph);
  10226. if (token == ';') {
  10227. getToken();
  10228. }
  10229. }
  10230. }
  10231. /**
  10232. * Parse a single statement. Can be a an attribute statement, node
  10233. * statement, a series of node statements and edge statements, or a
  10234. * parameter.
  10235. * @param {Object} graph
  10236. */
  10237. function parseStatement(graph) {
  10238. // parse subgraph
  10239. var subgraph = parseSubgraph(graph);
  10240. if (subgraph) {
  10241. // edge statements
  10242. parseEdge(graph, subgraph);
  10243. return;
  10244. }
  10245. // parse an attribute statement
  10246. var attr = parseAttributeStatement(graph);
  10247. if (attr) {
  10248. return;
  10249. }
  10250. // parse node
  10251. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10252. throw newSyntaxError('Identifier expected');
  10253. }
  10254. var id = token; // id can be a string or a number
  10255. getToken();
  10256. if (token == '=') {
  10257. // id statement
  10258. getToken();
  10259. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10260. throw newSyntaxError('Identifier expected');
  10261. }
  10262. graph[id] = token;
  10263. getToken();
  10264. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  10265. }
  10266. else {
  10267. parseNodeStatement(graph, id);
  10268. }
  10269. }
  10270. /**
  10271. * Parse a subgraph
  10272. * @param {Object} graph parent graph object
  10273. * @return {Object | null} subgraph
  10274. */
  10275. function parseSubgraph (graph) {
  10276. var subgraph = null;
  10277. // optional subgraph keyword
  10278. if (token == 'subgraph') {
  10279. subgraph = {};
  10280. subgraph.type = 'subgraph';
  10281. getToken();
  10282. // optional graph id
  10283. if (tokenType == TOKENTYPE.IDENTIFIER) {
  10284. subgraph.id = token;
  10285. getToken();
  10286. }
  10287. }
  10288. // open angle bracket
  10289. if (token == '{') {
  10290. getToken();
  10291. if (!subgraph) {
  10292. subgraph = {};
  10293. }
  10294. subgraph.parent = graph;
  10295. subgraph.node = graph.node;
  10296. subgraph.edge = graph.edge;
  10297. subgraph.graph = graph.graph;
  10298. // statements
  10299. parseStatements(subgraph);
  10300. // close angle bracket
  10301. if (token != '}') {
  10302. throw newSyntaxError('Angle bracket } expected');
  10303. }
  10304. getToken();
  10305. // remove temporary default properties
  10306. delete subgraph.node;
  10307. delete subgraph.edge;
  10308. delete subgraph.graph;
  10309. delete subgraph.parent;
  10310. // register at the parent graph
  10311. if (!graph.subgraphs) {
  10312. graph.subgraphs = [];
  10313. }
  10314. graph.subgraphs.push(subgraph);
  10315. }
  10316. return subgraph;
  10317. }
  10318. /**
  10319. * parse an attribute statement like "node [shape=circle fontSize=16]".
  10320. * Available keywords are 'node', 'edge', 'graph'.
  10321. * The previous list with default attributes will be replaced
  10322. * @param {Object} graph
  10323. * @returns {String | null} keyword Returns the name of the parsed attribute
  10324. * (node, edge, graph), or null if nothing
  10325. * is parsed.
  10326. */
  10327. function parseAttributeStatement (graph) {
  10328. // attribute statements
  10329. if (token == 'node') {
  10330. getToken();
  10331. // node attributes
  10332. graph.node = parseAttributeList();
  10333. return 'node';
  10334. }
  10335. else if (token == 'edge') {
  10336. getToken();
  10337. // edge attributes
  10338. graph.edge = parseAttributeList();
  10339. return 'edge';
  10340. }
  10341. else if (token == 'graph') {
  10342. getToken();
  10343. // graph attributes
  10344. graph.graph = parseAttributeList();
  10345. return 'graph';
  10346. }
  10347. return null;
  10348. }
  10349. /**
  10350. * parse a node statement
  10351. * @param {Object} graph
  10352. * @param {String | Number} id
  10353. */
  10354. function parseNodeStatement(graph, id) {
  10355. // node statement
  10356. var node = {
  10357. id: id
  10358. };
  10359. var attr = parseAttributeList();
  10360. if (attr) {
  10361. node.attr = attr;
  10362. }
  10363. addNode(graph, node);
  10364. // edge statements
  10365. parseEdge(graph, id);
  10366. }
  10367. /**
  10368. * Parse an edge or a series of edges
  10369. * @param {Object} graph
  10370. * @param {String | Number} from Id of the from node
  10371. */
  10372. function parseEdge(graph, from) {
  10373. while (token == '->' || token == '--') {
  10374. var to;
  10375. var type = token;
  10376. getToken();
  10377. var subgraph = parseSubgraph(graph);
  10378. if (subgraph) {
  10379. to = subgraph;
  10380. }
  10381. else {
  10382. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10383. throw newSyntaxError('Identifier or subgraph expected');
  10384. }
  10385. to = token;
  10386. addNode(graph, {
  10387. id: to
  10388. });
  10389. getToken();
  10390. }
  10391. // parse edge attributes
  10392. var attr = parseAttributeList();
  10393. // create edge
  10394. var edge = createEdge(graph, from, to, type, attr);
  10395. addEdge(graph, edge);
  10396. from = to;
  10397. }
  10398. }
  10399. /**
  10400. * Parse a set with attributes,
  10401. * for example [label="1.000", shape=solid]
  10402. * @return {Object | null} attr
  10403. */
  10404. function parseAttributeList() {
  10405. var attr = null;
  10406. while (token == '[') {
  10407. getToken();
  10408. attr = {};
  10409. while (token !== '' && token != ']') {
  10410. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10411. throw newSyntaxError('Attribute name expected');
  10412. }
  10413. var name = token;
  10414. getToken();
  10415. if (token != '=') {
  10416. throw newSyntaxError('Equal sign = expected');
  10417. }
  10418. getToken();
  10419. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10420. throw newSyntaxError('Attribute value expected');
  10421. }
  10422. var value = token;
  10423. setValue(attr, name, value); // name can be a path
  10424. getToken();
  10425. if (token ==',') {
  10426. getToken();
  10427. }
  10428. }
  10429. if (token != ']') {
  10430. throw newSyntaxError('Bracket ] expected');
  10431. }
  10432. getToken();
  10433. }
  10434. return attr;
  10435. }
  10436. /**
  10437. * Create a syntax error with extra information on current token and index.
  10438. * @param {String} message
  10439. * @returns {SyntaxError} err
  10440. */
  10441. function newSyntaxError(message) {
  10442. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  10443. }
  10444. /**
  10445. * Chop off text after a maximum length
  10446. * @param {String} text
  10447. * @param {Number} maxLength
  10448. * @returns {String}
  10449. */
  10450. function chop (text, maxLength) {
  10451. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  10452. }
  10453. /**
  10454. * Execute a function fn for each pair of elements in two arrays
  10455. * @param {Array | *} array1
  10456. * @param {Array | *} array2
  10457. * @param {function} fn
  10458. */
  10459. function forEach2(array1, array2, fn) {
  10460. if (array1 instanceof Array) {
  10461. array1.forEach(function (elem1) {
  10462. if (array2 instanceof Array) {
  10463. array2.forEach(function (elem2) {
  10464. fn(elem1, elem2);
  10465. });
  10466. }
  10467. else {
  10468. fn(elem1, array2);
  10469. }
  10470. });
  10471. }
  10472. else {
  10473. if (array2 instanceof Array) {
  10474. array2.forEach(function (elem2) {
  10475. fn(array1, elem2);
  10476. });
  10477. }
  10478. else {
  10479. fn(array1, array2);
  10480. }
  10481. }
  10482. }
  10483. /**
  10484. * Convert a string containing a graph in DOT language into a map containing
  10485. * with nodes and edges in the format of graph.
  10486. * @param {String} data Text containing a graph in DOT-notation
  10487. * @return {Object} graphData
  10488. */
  10489. function DOTToGraph (data) {
  10490. // parse the DOT file
  10491. var dotData = parseDOT(data);
  10492. var graphData = {
  10493. nodes: [],
  10494. edges: [],
  10495. options: {}
  10496. };
  10497. // copy the nodes
  10498. if (dotData.nodes) {
  10499. dotData.nodes.forEach(function (dotNode) {
  10500. var graphNode = {
  10501. id: dotNode.id,
  10502. label: String(dotNode.label || dotNode.id)
  10503. };
  10504. merge(graphNode, dotNode.attr);
  10505. if (graphNode.image) {
  10506. graphNode.shape = 'image';
  10507. }
  10508. graphData.nodes.push(graphNode);
  10509. });
  10510. }
  10511. // copy the edges
  10512. if (dotData.edges) {
  10513. /**
  10514. * Convert an edge in DOT format to an edge with VisGraph format
  10515. * @param {Object} dotEdge
  10516. * @returns {Object} graphEdge
  10517. */
  10518. function convertEdge(dotEdge) {
  10519. var graphEdge = {
  10520. from: dotEdge.from,
  10521. to: dotEdge.to
  10522. };
  10523. merge(graphEdge, dotEdge.attr);
  10524. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  10525. return graphEdge;
  10526. }
  10527. dotData.edges.forEach(function (dotEdge) {
  10528. var from, to;
  10529. if (dotEdge.from instanceof Object) {
  10530. from = dotEdge.from.nodes;
  10531. }
  10532. else {
  10533. from = {
  10534. id: dotEdge.from
  10535. }
  10536. }
  10537. if (dotEdge.to instanceof Object) {
  10538. to = dotEdge.to.nodes;
  10539. }
  10540. else {
  10541. to = {
  10542. id: dotEdge.to
  10543. }
  10544. }
  10545. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  10546. dotEdge.from.edges.forEach(function (subEdge) {
  10547. var graphEdge = convertEdge(subEdge);
  10548. graphData.edges.push(graphEdge);
  10549. });
  10550. }
  10551. forEach2(from, to, function (from, to) {
  10552. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  10553. var graphEdge = convertEdge(subEdge);
  10554. graphData.edges.push(graphEdge);
  10555. });
  10556. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  10557. dotEdge.to.edges.forEach(function (subEdge) {
  10558. var graphEdge = convertEdge(subEdge);
  10559. graphData.edges.push(graphEdge);
  10560. });
  10561. }
  10562. });
  10563. }
  10564. // copy the options
  10565. if (dotData.attr) {
  10566. graphData.options = dotData.attr;
  10567. }
  10568. return graphData;
  10569. }
  10570. // exports
  10571. exports.parseDOT = parseDOT;
  10572. exports.DOTToGraph = DOTToGraph;
  10573. })(typeof util !== 'undefined' ? util : exports);
  10574. /**
  10575. * Canvas shapes used by Network
  10576. */
  10577. if (typeof CanvasRenderingContext2D !== 'undefined') {
  10578. /**
  10579. * Draw a circle shape
  10580. */
  10581. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  10582. this.beginPath();
  10583. this.arc(x, y, r, 0, 2*Math.PI, false);
  10584. };
  10585. /**
  10586. * Draw a square shape
  10587. * @param {Number} x horizontal center
  10588. * @param {Number} y vertical center
  10589. * @param {Number} r size, width and height of the square
  10590. */
  10591. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  10592. this.beginPath();
  10593. this.rect(x - r, y - r, r * 2, r * 2);
  10594. };
  10595. /**
  10596. * Draw a triangle shape
  10597. * @param {Number} x horizontal center
  10598. * @param {Number} y vertical center
  10599. * @param {Number} r radius, half the length of the sides of the triangle
  10600. */
  10601. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  10602. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10603. this.beginPath();
  10604. var s = r * 2;
  10605. var s2 = s / 2;
  10606. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10607. var h = Math.sqrt(s * s - s2 * s2); // height
  10608. this.moveTo(x, y - (h - ir));
  10609. this.lineTo(x + s2, y + ir);
  10610. this.lineTo(x - s2, y + ir);
  10611. this.lineTo(x, y - (h - ir));
  10612. this.closePath();
  10613. };
  10614. /**
  10615. * Draw a triangle shape in downward orientation
  10616. * @param {Number} x horizontal center
  10617. * @param {Number} y vertical center
  10618. * @param {Number} r radius
  10619. */
  10620. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  10621. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10622. this.beginPath();
  10623. var s = r * 2;
  10624. var s2 = s / 2;
  10625. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10626. var h = Math.sqrt(s * s - s2 * s2); // height
  10627. this.moveTo(x, y + (h - ir));
  10628. this.lineTo(x + s2, y - ir);
  10629. this.lineTo(x - s2, y - ir);
  10630. this.lineTo(x, y + (h - ir));
  10631. this.closePath();
  10632. };
  10633. /**
  10634. * Draw a star shape, a star with 5 points
  10635. * @param {Number} x horizontal center
  10636. * @param {Number} y vertical center
  10637. * @param {Number} r radius, half the length of the sides of the triangle
  10638. */
  10639. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  10640. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  10641. this.beginPath();
  10642. for (var n = 0; n < 10; n++) {
  10643. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  10644. this.lineTo(
  10645. x + radius * Math.sin(n * 2 * Math.PI / 10),
  10646. y - radius * Math.cos(n * 2 * Math.PI / 10)
  10647. );
  10648. }
  10649. this.closePath();
  10650. };
  10651. /**
  10652. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  10653. */
  10654. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  10655. var r2d = Math.PI/180;
  10656. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  10657. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  10658. this.beginPath();
  10659. this.moveTo(x+r,y);
  10660. this.lineTo(x+w-r,y);
  10661. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  10662. this.lineTo(x+w,y+h-r);
  10663. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  10664. this.lineTo(x+r,y+h);
  10665. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  10666. this.lineTo(x,y+r);
  10667. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  10668. };
  10669. /**
  10670. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10671. */
  10672. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  10673. var kappa = .5522848,
  10674. ox = (w / 2) * kappa, // control point offset horizontal
  10675. oy = (h / 2) * kappa, // control point offset vertical
  10676. xe = x + w, // x-end
  10677. ye = y + h, // y-end
  10678. xm = x + w / 2, // x-middle
  10679. ym = y + h / 2; // y-middle
  10680. this.beginPath();
  10681. this.moveTo(x, ym);
  10682. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10683. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10684. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10685. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10686. };
  10687. /**
  10688. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10689. */
  10690. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  10691. var f = 1/3;
  10692. var wEllipse = w;
  10693. var hEllipse = h * f;
  10694. var kappa = .5522848,
  10695. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  10696. oy = (hEllipse / 2) * kappa, // control point offset vertical
  10697. xe = x + wEllipse, // x-end
  10698. ye = y + hEllipse, // y-end
  10699. xm = x + wEllipse / 2, // x-middle
  10700. ym = y + hEllipse / 2, // y-middle
  10701. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  10702. yeb = y + h; // y-end, bottom ellipse
  10703. this.beginPath();
  10704. this.moveTo(xe, ym);
  10705. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10706. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10707. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10708. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10709. this.lineTo(xe, ymb);
  10710. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  10711. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  10712. this.lineTo(x, ym);
  10713. };
  10714. /**
  10715. * Draw an arrow point (no line)
  10716. */
  10717. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  10718. // tail
  10719. var xt = x - length * Math.cos(angle);
  10720. var yt = y - length * Math.sin(angle);
  10721. // inner tail
  10722. // TODO: allow to customize different shapes
  10723. var xi = x - length * 0.9 * Math.cos(angle);
  10724. var yi = y - length * 0.9 * Math.sin(angle);
  10725. // left
  10726. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  10727. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  10728. // right
  10729. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  10730. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  10731. this.beginPath();
  10732. this.moveTo(x, y);
  10733. this.lineTo(xl, yl);
  10734. this.lineTo(xi, yi);
  10735. this.lineTo(xr, yr);
  10736. this.closePath();
  10737. };
  10738. /**
  10739. * Sets up the dashedLine functionality for drawing
  10740. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  10741. * @author David Jordan
  10742. * @date 2012-08-08
  10743. */
  10744. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  10745. if (!dashArray) dashArray=[10,5];
  10746. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  10747. var dashCount = dashArray.length;
  10748. this.moveTo(x, y);
  10749. var dx = (x2-x), dy = (y2-y);
  10750. var slope = dy/dx;
  10751. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  10752. var dashIndex=0, draw=true;
  10753. while (distRemaining>=0.1){
  10754. var dashLength = dashArray[dashIndex++%dashCount];
  10755. if (dashLength > distRemaining) dashLength = distRemaining;
  10756. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  10757. if (dx<0) xStep = -xStep;
  10758. x += xStep;
  10759. y += slope*xStep;
  10760. this[draw ? 'lineTo' : 'moveTo'](x,y);
  10761. distRemaining -= dashLength;
  10762. draw = !draw;
  10763. }
  10764. };
  10765. // TODO: add diamond shape
  10766. }
  10767. /**
  10768. * @class Node
  10769. * A node. A node can be connected to other nodes via one or multiple edges.
  10770. * @param {object} properties An object containing properties for the node. All
  10771. * properties are optional, except for the id.
  10772. * {number} id Id of the node. Required
  10773. * {string} label Text label for the node
  10774. * {number} x Horizontal position of the node
  10775. * {number} y Vertical position of the node
  10776. * {string} shape Node shape, available:
  10777. * "database", "circle", "ellipse",
  10778. * "box", "image", "text", "dot",
  10779. * "star", "triangle", "triangleDown",
  10780. * "square"
  10781. * {string} image An image url
  10782. * {string} title An title text, can be HTML
  10783. * {anytype} group A group name or number
  10784. * @param {Network.Images} imagelist A list with images. Only needed
  10785. * when the node has an image
  10786. * @param {Network.Groups} grouplist A list with groups. Needed for
  10787. * retrieving group properties
  10788. * @param {Object} constants An object with default values for
  10789. * example for the color
  10790. *
  10791. */
  10792. function Node(properties, imagelist, grouplist, constants) {
  10793. this.selected = false;
  10794. this.hover = false;
  10795. this.edges = []; // all edges connected to this node
  10796. this.dynamicEdges = [];
  10797. this.reroutedEdges = {};
  10798. this.group = constants.nodes.group;
  10799. this.fontSize = Number(constants.nodes.fontSize);
  10800. this.fontFace = constants.nodes.fontFace;
  10801. this.fontColor = constants.nodes.fontColor;
  10802. this.fontDrawThreshold = 3;
  10803. this.color = constants.nodes.color;
  10804. // set defaults for the properties
  10805. this.id = undefined;
  10806. this.shape = constants.nodes.shape;
  10807. this.image = constants.nodes.image;
  10808. this.x = null;
  10809. this.y = null;
  10810. this.xFixed = false;
  10811. this.yFixed = false;
  10812. this.horizontalAlignLeft = true; // these are for the navigation controls
  10813. this.verticalAlignTop = true; // these are for the navigation controls
  10814. this.radius = constants.nodes.radius;
  10815. this.baseRadiusValue = constants.nodes.radius;
  10816. this.radiusFixed = false;
  10817. this.radiusMin = constants.nodes.radiusMin;
  10818. this.radiusMax = constants.nodes.radiusMax;
  10819. this.level = -1;
  10820. this.preassignedLevel = false;
  10821. this.imagelist = imagelist;
  10822. this.grouplist = grouplist;
  10823. // physics properties
  10824. this.fx = 0.0; // external force x
  10825. this.fy = 0.0; // external force y
  10826. this.vx = 0.0; // velocity x
  10827. this.vy = 0.0; // velocity y
  10828. this.minForce = constants.minForce;
  10829. this.damping = constants.physics.damping;
  10830. this.mass = 1; // kg
  10831. this.fixedData = {x:null,y:null};
  10832. this.setProperties(properties, constants);
  10833. // creating the variables for clustering
  10834. this.resetCluster();
  10835. this.dynamicEdgesLength = 0;
  10836. this.clusterSession = 0;
  10837. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  10838. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  10839. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  10840. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  10841. this.growthIndicator = 0;
  10842. // variables to tell the node about the network.
  10843. this.networkScaleInv = 1;
  10844. this.networkScale = 1;
  10845. this.canvasTopLeft = {"x": -300, "y": -300};
  10846. this.canvasBottomRight = {"x": 300, "y": 300};
  10847. this.parentEdgeId = null;
  10848. }
  10849. /**
  10850. * (re)setting the clustering variables and objects
  10851. */
  10852. Node.prototype.resetCluster = function() {
  10853. // clustering variables
  10854. this.formationScale = undefined; // this is used to determine when to open the cluster
  10855. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  10856. this.containedNodes = {};
  10857. this.containedEdges = {};
  10858. this.clusterSessions = [];
  10859. };
  10860. /**
  10861. * Attach a edge to the node
  10862. * @param {Edge} edge
  10863. */
  10864. Node.prototype.attachEdge = function(edge) {
  10865. if (this.edges.indexOf(edge) == -1) {
  10866. this.edges.push(edge);
  10867. }
  10868. if (this.dynamicEdges.indexOf(edge) == -1) {
  10869. this.dynamicEdges.push(edge);
  10870. }
  10871. this.dynamicEdgesLength = this.dynamicEdges.length;
  10872. };
  10873. /**
  10874. * Detach a edge from the node
  10875. * @param {Edge} edge
  10876. */
  10877. Node.prototype.detachEdge = function(edge) {
  10878. var index = this.edges.indexOf(edge);
  10879. if (index != -1) {
  10880. this.edges.splice(index, 1);
  10881. this.dynamicEdges.splice(index, 1);
  10882. }
  10883. this.dynamicEdgesLength = this.dynamicEdges.length;
  10884. };
  10885. /**
  10886. * Set or overwrite properties for the node
  10887. * @param {Object} properties an object with properties
  10888. * @param {Object} constants and object with default, global properties
  10889. */
  10890. Node.prototype.setProperties = function(properties, constants) {
  10891. if (!properties) {
  10892. return;
  10893. }
  10894. this.originalLabel = undefined;
  10895. // basic properties
  10896. if (properties.id !== undefined) {this.id = properties.id;}
  10897. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  10898. if (properties.title !== undefined) {this.title = properties.title;}
  10899. if (properties.group !== undefined) {this.group = properties.group;}
  10900. if (properties.x !== undefined) {this.x = properties.x;}
  10901. if (properties.y !== undefined) {this.y = properties.y;}
  10902. if (properties.value !== undefined) {this.value = properties.value;}
  10903. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  10904. // physics
  10905. if (properties.mass !== undefined) {this.mass = properties.mass;}
  10906. // navigation controls properties
  10907. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  10908. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  10909. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  10910. if (this.id === undefined) {
  10911. throw "Node must have an id";
  10912. }
  10913. // copy group properties
  10914. if (this.group) {
  10915. var groupObj = this.grouplist.get(this.group);
  10916. for (var prop in groupObj) {
  10917. if (groupObj.hasOwnProperty(prop)) {
  10918. this[prop] = groupObj[prop];
  10919. }
  10920. }
  10921. }
  10922. // individual shape properties
  10923. if (properties.shape !== undefined) {this.shape = properties.shape;}
  10924. if (properties.image !== undefined) {this.image = properties.image;}
  10925. if (properties.radius !== undefined) {this.radius = properties.radius;}
  10926. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  10927. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  10928. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  10929. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  10930. if (this.image !== undefined && this.image != "") {
  10931. if (this.imagelist) {
  10932. this.imageObj = this.imagelist.load(this.image);
  10933. }
  10934. else {
  10935. throw "No imagelist provided";
  10936. }
  10937. }
  10938. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  10939. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  10940. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  10941. if (this.shape == 'image') {
  10942. this.radiusMin = constants.nodes.widthMin;
  10943. this.radiusMax = constants.nodes.widthMax;
  10944. }
  10945. // choose draw method depending on the shape
  10946. switch (this.shape) {
  10947. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  10948. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  10949. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  10950. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10951. // TODO: add diamond shape
  10952. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  10953. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  10954. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  10955. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  10956. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  10957. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  10958. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  10959. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10960. }
  10961. // reset the size of the node, this can be changed
  10962. this._reset();
  10963. };
  10964. /**
  10965. * select this node
  10966. */
  10967. Node.prototype.select = function() {
  10968. this.selected = true;
  10969. this._reset();
  10970. };
  10971. /**
  10972. * unselect this node
  10973. */
  10974. Node.prototype.unselect = function() {
  10975. this.selected = false;
  10976. this._reset();
  10977. };
  10978. /**
  10979. * Reset the calculated size of the node, forces it to recalculate its size
  10980. */
  10981. Node.prototype.clearSizeCache = function() {
  10982. this._reset();
  10983. };
  10984. /**
  10985. * Reset the calculated size of the node, forces it to recalculate its size
  10986. * @private
  10987. */
  10988. Node.prototype._reset = function() {
  10989. this.width = undefined;
  10990. this.height = undefined;
  10991. };
  10992. /**
  10993. * get the title of this node.
  10994. * @return {string} title The title of the node, or undefined when no title
  10995. * has been set.
  10996. */
  10997. Node.prototype.getTitle = function() {
  10998. return typeof this.title === "function" ? this.title() : this.title;
  10999. };
  11000. /**
  11001. * Calculate the distance to the border of the Node
  11002. * @param {CanvasRenderingContext2D} ctx
  11003. * @param {Number} angle Angle in radians
  11004. * @returns {number} distance Distance to the border in pixels
  11005. */
  11006. Node.prototype.distanceToBorder = function (ctx, angle) {
  11007. var borderWidth = 1;
  11008. if (!this.width) {
  11009. this.resize(ctx);
  11010. }
  11011. switch (this.shape) {
  11012. case 'circle':
  11013. case 'dot':
  11014. return this.radius + borderWidth;
  11015. case 'ellipse':
  11016. var a = this.width / 2;
  11017. var b = this.height / 2;
  11018. var w = (Math.sin(angle) * a);
  11019. var h = (Math.cos(angle) * b);
  11020. return a * b / Math.sqrt(w * w + h * h);
  11021. // TODO: implement distanceToBorder for database
  11022. // TODO: implement distanceToBorder for triangle
  11023. // TODO: implement distanceToBorder for triangleDown
  11024. case 'box':
  11025. case 'image':
  11026. case 'text':
  11027. default:
  11028. if (this.width) {
  11029. return Math.min(
  11030. Math.abs(this.width / 2 / Math.cos(angle)),
  11031. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  11032. // TODO: reckon with border radius too in case of box
  11033. }
  11034. else {
  11035. return 0;
  11036. }
  11037. }
  11038. // TODO: implement calculation of distance to border for all shapes
  11039. };
  11040. /**
  11041. * Set forces acting on the node
  11042. * @param {number} fx Force in horizontal direction
  11043. * @param {number} fy Force in vertical direction
  11044. */
  11045. Node.prototype._setForce = function(fx, fy) {
  11046. this.fx = fx;
  11047. this.fy = fy;
  11048. };
  11049. /**
  11050. * Add forces acting on the node
  11051. * @param {number} fx Force in horizontal direction
  11052. * @param {number} fy Force in vertical direction
  11053. * @private
  11054. */
  11055. Node.prototype._addForce = function(fx, fy) {
  11056. this.fx += fx;
  11057. this.fy += fy;
  11058. };
  11059. /**
  11060. * Perform one discrete step for the node
  11061. * @param {number} interval Time interval in seconds
  11062. */
  11063. Node.prototype.discreteStep = function(interval) {
  11064. if (!this.xFixed) {
  11065. var dx = this.damping * this.vx; // damping force
  11066. var ax = (this.fx - dx) / this.mass; // acceleration
  11067. this.vx += ax * interval; // velocity
  11068. this.x += this.vx * interval; // position
  11069. }
  11070. if (!this.yFixed) {
  11071. var dy = this.damping * this.vy; // damping force
  11072. var ay = (this.fy - dy) / this.mass; // acceleration
  11073. this.vy += ay * interval; // velocity
  11074. this.y += this.vy * interval; // position
  11075. }
  11076. };
  11077. /**
  11078. * Perform one discrete step for the node
  11079. * @param {number} interval Time interval in seconds
  11080. * @param {number} maxVelocity The speed limit imposed on the velocity
  11081. */
  11082. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  11083. if (!this.xFixed) {
  11084. var dx = this.damping * this.vx; // damping force
  11085. var ax = (this.fx - dx) / this.mass; // acceleration
  11086. this.vx += ax * interval; // velocity
  11087. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  11088. this.x += this.vx * interval; // position
  11089. }
  11090. else {
  11091. this.fx = 0;
  11092. }
  11093. if (!this.yFixed) {
  11094. var dy = this.damping * this.vy; // damping force
  11095. var ay = (this.fy - dy) / this.mass; // acceleration
  11096. this.vy += ay * interval; // velocity
  11097. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  11098. this.y += this.vy * interval; // position
  11099. }
  11100. else {
  11101. this.fy = 0;
  11102. }
  11103. };
  11104. /**
  11105. * Check if this node has a fixed x and y position
  11106. * @return {boolean} true if fixed, false if not
  11107. */
  11108. Node.prototype.isFixed = function() {
  11109. return (this.xFixed && this.yFixed);
  11110. };
  11111. /**
  11112. * Check if this node is moving
  11113. * @param {number} vmin the minimum velocity considered as "moving"
  11114. * @return {boolean} true if moving, false if it has no velocity
  11115. */
  11116. // TODO: replace this method with calculating the kinetic energy
  11117. Node.prototype.isMoving = function(vmin) {
  11118. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  11119. };
  11120. /**
  11121. * check if this node is selecte
  11122. * @return {boolean} selected True if node is selected, else false
  11123. */
  11124. Node.prototype.isSelected = function() {
  11125. return this.selected;
  11126. };
  11127. /**
  11128. * Retrieve the value of the node. Can be undefined
  11129. * @return {Number} value
  11130. */
  11131. Node.prototype.getValue = function() {
  11132. return this.value;
  11133. };
  11134. /**
  11135. * Calculate the distance from the nodes location to the given location (x,y)
  11136. * @param {Number} x
  11137. * @param {Number} y
  11138. * @return {Number} value
  11139. */
  11140. Node.prototype.getDistance = function(x, y) {
  11141. var dx = this.x - x,
  11142. dy = this.y - y;
  11143. return Math.sqrt(dx * dx + dy * dy);
  11144. };
  11145. /**
  11146. * Adjust the value range of the node. The node will adjust it's radius
  11147. * based on its value.
  11148. * @param {Number} min
  11149. * @param {Number} max
  11150. */
  11151. Node.prototype.setValueRange = function(min, max) {
  11152. if (!this.radiusFixed && this.value !== undefined) {
  11153. if (max == min) {
  11154. this.radius = (this.radiusMin + this.radiusMax) / 2;
  11155. }
  11156. else {
  11157. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  11158. this.radius = (this.value - min) * scale + this.radiusMin;
  11159. }
  11160. }
  11161. this.baseRadiusValue = this.radius;
  11162. };
  11163. /**
  11164. * Draw this node in the given canvas
  11165. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11166. * @param {CanvasRenderingContext2D} ctx
  11167. */
  11168. Node.prototype.draw = function(ctx) {
  11169. throw "Draw method not initialized for node";
  11170. };
  11171. /**
  11172. * Recalculate the size of this node in the given canvas
  11173. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11174. * @param {CanvasRenderingContext2D} ctx
  11175. */
  11176. Node.prototype.resize = function(ctx) {
  11177. throw "Resize method not initialized for node";
  11178. };
  11179. /**
  11180. * Check if this object is overlapping with the provided object
  11181. * @param {Object} obj an object with parameters left, top, right, bottom
  11182. * @return {boolean} True if location is located on node
  11183. */
  11184. Node.prototype.isOverlappingWith = function(obj) {
  11185. return (this.left < obj.right &&
  11186. this.left + this.width > obj.left &&
  11187. this.top < obj.bottom &&
  11188. this.top + this.height > obj.top);
  11189. };
  11190. Node.prototype._resizeImage = function (ctx) {
  11191. // TODO: pre calculate the image size
  11192. if (!this.width || !this.height) { // undefined or 0
  11193. var width, height;
  11194. if (this.value) {
  11195. this.radius = this.baseRadiusValue;
  11196. var scale = this.imageObj.height / this.imageObj.width;
  11197. if (scale !== undefined) {
  11198. width = this.radius || this.imageObj.width;
  11199. height = this.radius * scale || this.imageObj.height;
  11200. }
  11201. else {
  11202. width = 0;
  11203. height = 0;
  11204. }
  11205. }
  11206. else {
  11207. width = this.imageObj.width;
  11208. height = this.imageObj.height;
  11209. }
  11210. this.width = width;
  11211. this.height = height;
  11212. this.growthIndicator = 0;
  11213. if (this.width > 0 && this.height > 0) {
  11214. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11215. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11216. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11217. this.growthIndicator = this.width - width;
  11218. }
  11219. }
  11220. };
  11221. Node.prototype._drawImage = function (ctx) {
  11222. this._resizeImage(ctx);
  11223. this.left = this.x - this.width / 2;
  11224. this.top = this.y - this.height / 2;
  11225. var yLabel;
  11226. if (this.imageObj.width != 0 ) {
  11227. // draw the shade
  11228. if (this.clusterSize > 1) {
  11229. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  11230. lineWidth *= this.networkScaleInv;
  11231. lineWidth = Math.min(0.2 * this.width,lineWidth);
  11232. ctx.globalAlpha = 0.5;
  11233. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  11234. }
  11235. // draw the image
  11236. ctx.globalAlpha = 1.0;
  11237. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  11238. yLabel = this.y + this.height / 2;
  11239. }
  11240. else {
  11241. // image still loading... just draw the label for now
  11242. yLabel = this.y;
  11243. }
  11244. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  11245. };
  11246. Node.prototype._resizeBox = function (ctx) {
  11247. if (!this.width) {
  11248. var margin = 5;
  11249. var textSize = this.getTextSize(ctx);
  11250. this.width = textSize.width + 2 * margin;
  11251. this.height = textSize.height + 2 * margin;
  11252. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  11253. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  11254. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  11255. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  11256. }
  11257. };
  11258. Node.prototype._drawBox = function (ctx) {
  11259. this._resizeBox(ctx);
  11260. this.left = this.x - this.width / 2;
  11261. this.top = this.y - this.height / 2;
  11262. var clusterLineWidth = 2.5;
  11263. var selectionLineWidth = 2;
  11264. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11265. // draw the outer border
  11266. if (this.clusterSize > 1) {
  11267. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11268. ctx.lineWidth *= this.networkScaleInv;
  11269. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11270. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  11271. ctx.stroke();
  11272. }
  11273. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11274. ctx.lineWidth *= this.networkScaleInv;
  11275. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11276. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11277. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  11278. ctx.fill();
  11279. ctx.stroke();
  11280. this._label(ctx, this.label, this.x, this.y);
  11281. };
  11282. Node.prototype._resizeDatabase = function (ctx) {
  11283. if (!this.width) {
  11284. var margin = 5;
  11285. var textSize = this.getTextSize(ctx);
  11286. var size = textSize.width + 2 * margin;
  11287. this.width = size;
  11288. this.height = size;
  11289. // scaling used for clustering
  11290. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11291. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11292. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11293. this.growthIndicator = this.width - size;
  11294. }
  11295. };
  11296. Node.prototype._drawDatabase = function (ctx) {
  11297. this._resizeDatabase(ctx);
  11298. this.left = this.x - this.width / 2;
  11299. this.top = this.y - this.height / 2;
  11300. var clusterLineWidth = 2.5;
  11301. var selectionLineWidth = 2;
  11302. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11303. // draw the outer border
  11304. if (this.clusterSize > 1) {
  11305. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11306. ctx.lineWidth *= this.networkScaleInv;
  11307. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11308. 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);
  11309. ctx.stroke();
  11310. }
  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.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11315. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  11316. ctx.fill();
  11317. ctx.stroke();
  11318. this._label(ctx, this.label, this.x, this.y);
  11319. };
  11320. Node.prototype._resizeCircle = function (ctx) {
  11321. if (!this.width) {
  11322. var margin = 5;
  11323. var textSize = this.getTextSize(ctx);
  11324. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  11325. this.radius = diameter / 2;
  11326. this.width = diameter;
  11327. this.height = diameter;
  11328. // scaling used for clustering
  11329. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  11330. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  11331. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  11332. this.growthIndicator = this.radius - 0.5*diameter;
  11333. }
  11334. };
  11335. Node.prototype._drawCircle = function (ctx) {
  11336. this._resizeCircle(ctx);
  11337. this.left = this.x - this.width / 2;
  11338. this.top = this.y - this.height / 2;
  11339. var clusterLineWidth = 2.5;
  11340. var selectionLineWidth = 2;
  11341. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11342. // draw the outer border
  11343. if (this.clusterSize > 1) {
  11344. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11345. ctx.lineWidth *= this.networkScaleInv;
  11346. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11347. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  11348. ctx.stroke();
  11349. }
  11350. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11351. ctx.lineWidth *= this.networkScaleInv;
  11352. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11353. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11354. ctx.circle(this.x, this.y, this.radius);
  11355. ctx.fill();
  11356. ctx.stroke();
  11357. this._label(ctx, this.label, this.x, this.y);
  11358. };
  11359. Node.prototype._resizeEllipse = function (ctx) {
  11360. if (!this.width) {
  11361. var textSize = this.getTextSize(ctx);
  11362. this.width = textSize.width * 1.5;
  11363. this.height = textSize.height * 2;
  11364. if (this.width < this.height) {
  11365. this.width = this.height;
  11366. }
  11367. var defaultSize = this.width;
  11368. // scaling used for clustering
  11369. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11370. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11371. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11372. this.growthIndicator = this.width - defaultSize;
  11373. }
  11374. };
  11375. Node.prototype._drawEllipse = function (ctx) {
  11376. this._resizeEllipse(ctx);
  11377. this.left = this.x - this.width / 2;
  11378. this.top = this.y - this.height / 2;
  11379. var clusterLineWidth = 2.5;
  11380. var selectionLineWidth = 2;
  11381. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11382. // draw the outer border
  11383. if (this.clusterSize > 1) {
  11384. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11385. ctx.lineWidth *= this.networkScaleInv;
  11386. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11387. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  11388. ctx.stroke();
  11389. }
  11390. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11391. ctx.lineWidth *= this.networkScaleInv;
  11392. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11393. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11394. ctx.ellipse(this.left, this.top, this.width, this.height);
  11395. ctx.fill();
  11396. ctx.stroke();
  11397. this._label(ctx, this.label, this.x, this.y);
  11398. };
  11399. Node.prototype._drawDot = function (ctx) {
  11400. this._drawShape(ctx, 'circle');
  11401. };
  11402. Node.prototype._drawTriangle = function (ctx) {
  11403. this._drawShape(ctx, 'triangle');
  11404. };
  11405. Node.prototype._drawTriangleDown = function (ctx) {
  11406. this._drawShape(ctx, 'triangleDown');
  11407. };
  11408. Node.prototype._drawSquare = function (ctx) {
  11409. this._drawShape(ctx, 'square');
  11410. };
  11411. Node.prototype._drawStar = function (ctx) {
  11412. this._drawShape(ctx, 'star');
  11413. };
  11414. Node.prototype._resizeShape = function (ctx) {
  11415. if (!this.width) {
  11416. this.radius = this.baseRadiusValue;
  11417. var size = 2 * this.radius;
  11418. this.width = size;
  11419. this.height = size;
  11420. // scaling used for clustering
  11421. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11422. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11423. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  11424. this.growthIndicator = this.width - size;
  11425. }
  11426. };
  11427. Node.prototype._drawShape = function (ctx, shape) {
  11428. this._resizeShape(ctx);
  11429. this.left = this.x - this.width / 2;
  11430. this.top = this.y - this.height / 2;
  11431. var clusterLineWidth = 2.5;
  11432. var selectionLineWidth = 2;
  11433. var radiusMultiplier = 2;
  11434. // choose draw method depending on the shape
  11435. switch (shape) {
  11436. case 'dot': radiusMultiplier = 2; break;
  11437. case 'square': radiusMultiplier = 2; break;
  11438. case 'triangle': radiusMultiplier = 3; break;
  11439. case 'triangleDown': radiusMultiplier = 3; break;
  11440. case 'star': radiusMultiplier = 4; break;
  11441. }
  11442. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  11443. // draw the outer border
  11444. if (this.clusterSize > 1) {
  11445. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11446. ctx.lineWidth *= this.networkScaleInv;
  11447. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11448. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  11449. ctx.stroke();
  11450. }
  11451. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  11452. ctx.lineWidth *= this.networkScaleInv;
  11453. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  11454. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  11455. ctx[shape](this.x, this.y, this.radius);
  11456. ctx.fill();
  11457. ctx.stroke();
  11458. if (this.label) {
  11459. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true);
  11460. }
  11461. };
  11462. Node.prototype._resizeText = function (ctx) {
  11463. if (!this.width) {
  11464. var margin = 5;
  11465. var textSize = this.getTextSize(ctx);
  11466. this.width = textSize.width + 2 * margin;
  11467. this.height = textSize.height + 2 * margin;
  11468. // scaling used for clustering
  11469. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  11470. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  11471. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  11472. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  11473. }
  11474. };
  11475. Node.prototype._drawText = function (ctx) {
  11476. this._resizeText(ctx);
  11477. this.left = this.x - this.width / 2;
  11478. this.top = this.y - this.height / 2;
  11479. this._label(ctx, this.label, this.x, this.y);
  11480. };
  11481. Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
  11482. if (text && this.fontSize * this.networkScale > this.fontDrawThreshold) {
  11483. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  11484. ctx.fillStyle = this.fontColor || "black";
  11485. ctx.textAlign = align || "center";
  11486. ctx.textBaseline = baseline || "middle";
  11487. var lines = text.split('\n');
  11488. var lineCount = lines.length;
  11489. var fontSize = (this.fontSize + 4);
  11490. var yLine = y + (1 - lineCount) / 2 * fontSize;
  11491. if (labelUnderNode == true) {
  11492. yLine = y + (1 - lineCount) / (2 * fontSize);
  11493. }
  11494. for (var i = 0; i < lineCount; i++) {
  11495. ctx.fillText(lines[i], x, yLine);
  11496. yLine += fontSize;
  11497. }
  11498. }
  11499. };
  11500. Node.prototype.getTextSize = function(ctx) {
  11501. if (this.label !== undefined) {
  11502. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  11503. var lines = this.label.split('\n'),
  11504. height = (this.fontSize + 4) * lines.length,
  11505. width = 0;
  11506. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  11507. width = Math.max(width, ctx.measureText(lines[i]).width);
  11508. }
  11509. return {"width": width, "height": height};
  11510. }
  11511. else {
  11512. return {"width": 0, "height": 0};
  11513. }
  11514. };
  11515. /**
  11516. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  11517. * there is a safety margin of 0.3 * width;
  11518. *
  11519. * @returns {boolean}
  11520. */
  11521. Node.prototype.inArea = function() {
  11522. if (this.width !== undefined) {
  11523. return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x &&
  11524. this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x &&
  11525. this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y &&
  11526. this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y);
  11527. }
  11528. else {
  11529. return true;
  11530. }
  11531. };
  11532. /**
  11533. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  11534. * @returns {boolean}
  11535. */
  11536. Node.prototype.inView = function() {
  11537. return (this.x >= this.canvasTopLeft.x &&
  11538. this.x < this.canvasBottomRight.x &&
  11539. this.y >= this.canvasTopLeft.y &&
  11540. this.y < this.canvasBottomRight.y);
  11541. };
  11542. /**
  11543. * This allows the zoom level of the network to influence the rendering
  11544. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  11545. *
  11546. * @param scale
  11547. * @param canvasTopLeft
  11548. * @param canvasBottomRight
  11549. */
  11550. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  11551. this.networkScaleInv = 1.0/scale;
  11552. this.networkScale = scale;
  11553. this.canvasTopLeft = canvasTopLeft;
  11554. this.canvasBottomRight = canvasBottomRight;
  11555. };
  11556. /**
  11557. * This allows the zoom level of the network to influence the rendering
  11558. *
  11559. * @param scale
  11560. */
  11561. Node.prototype.setScale = function(scale) {
  11562. this.networkScaleInv = 1.0/scale;
  11563. this.networkScale = scale;
  11564. };
  11565. /**
  11566. * set the velocity at 0. Is called when this node is contained in another during clustering
  11567. */
  11568. Node.prototype.clearVelocity = function() {
  11569. this.vx = 0;
  11570. this.vy = 0;
  11571. };
  11572. /**
  11573. * Basic preservation of (kinectic) energy
  11574. *
  11575. * @param massBeforeClustering
  11576. */
  11577. Node.prototype.updateVelocity = function(massBeforeClustering) {
  11578. var energyBefore = this.vx * this.vx * massBeforeClustering;
  11579. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  11580. this.vx = Math.sqrt(energyBefore/this.mass);
  11581. energyBefore = this.vy * this.vy * massBeforeClustering;
  11582. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  11583. this.vy = Math.sqrt(energyBefore/this.mass);
  11584. };
  11585. /**
  11586. * @class Edge
  11587. *
  11588. * A edge connects two nodes
  11589. * @param {Object} properties Object with properties. Must contain
  11590. * At least properties from and to.
  11591. * Available properties: from (number),
  11592. * to (number), label (string, color (string),
  11593. * width (number), style (string),
  11594. * length (number), title (string)
  11595. * @param {Network} network A Network object, used to find and edge to
  11596. * nodes.
  11597. * @param {Object} constants An object with default values for
  11598. * example for the color
  11599. */
  11600. function Edge (properties, network, constants) {
  11601. if (!network) {
  11602. throw "No network provided";
  11603. }
  11604. this.network = network;
  11605. // initialize constants
  11606. this.widthMin = constants.edges.widthMin;
  11607. this.widthMax = constants.edges.widthMax;
  11608. // initialize variables
  11609. this.id = undefined;
  11610. this.fromId = undefined;
  11611. this.toId = undefined;
  11612. this.style = constants.edges.style;
  11613. this.title = undefined;
  11614. this.width = constants.edges.width;
  11615. this.widthSelectionMultiplier = constants.edges.widthSelectionMultiplier;
  11616. this.widthSelected = this.width * this.widthSelectionMultiplier;
  11617. this.hoverWidth = constants.edges.hoverWidth;
  11618. this.value = undefined;
  11619. this.length = constants.physics.springLength;
  11620. this.customLength = false;
  11621. this.selected = false;
  11622. this.hover = false;
  11623. this.smooth = constants.smoothCurves;
  11624. this.arrowScaleFactor = constants.edges.arrowScaleFactor;
  11625. this.from = null; // a node
  11626. this.to = null; // a node
  11627. this.via = null; // a temp node
  11628. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  11629. // by storing the original information we can revert to the original connection when the cluser is opened.
  11630. this.originalFromId = [];
  11631. this.originalToId = [];
  11632. this.connected = false;
  11633. // Added to support dashed lines
  11634. // David Jordan
  11635. // 2012-08-08
  11636. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  11637. this.color = {color:constants.edges.color.color,
  11638. highlight:constants.edges.color.highlight,
  11639. hover:constants.edges.color.hover};
  11640. this.widthFixed = false;
  11641. this.lengthFixed = false;
  11642. this.setProperties(properties, constants);
  11643. this.controlNodesEnabled = false;
  11644. this.controlNodes = {from:null, to:null, positions:{}};
  11645. this.connectedNode = null;
  11646. }
  11647. /**
  11648. * Set or overwrite properties for the edge
  11649. * @param {Object} properties an object with properties
  11650. * @param {Object} constants and object with default, global properties
  11651. */
  11652. Edge.prototype.setProperties = function(properties, constants) {
  11653. if (!properties) {
  11654. return;
  11655. }
  11656. if (properties.from !== undefined) {this.fromId = properties.from;}
  11657. if (properties.to !== undefined) {this.toId = properties.to;}
  11658. if (properties.id !== undefined) {this.id = properties.id;}
  11659. if (properties.style !== undefined) {this.style = properties.style;}
  11660. if (properties.label !== undefined) {this.label = properties.label;}
  11661. if (this.label) {
  11662. this.fontSize = constants.edges.fontSize;
  11663. this.fontFace = constants.edges.fontFace;
  11664. this.fontColor = constants.edges.fontColor;
  11665. this.fontFill = constants.edges.fontFill;
  11666. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  11667. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  11668. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  11669. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  11670. }
  11671. if (properties.title !== undefined) {this.title = properties.title;}
  11672. if (properties.width !== undefined) {this.width = properties.width;}
  11673. if (properties.widthSelectionMultiplier !== undefined)
  11674. {this.widthSelectionMultiplier = properties.widthSelectionMultiplier;}
  11675. if (properties.hoverWidth !== undefined) {this.hoverWidth = properties.hoverWidth;}
  11676. if (properties.value !== undefined) {this.value = properties.value;}
  11677. if (properties.length !== undefined) {this.length = properties.length;
  11678. this.customLength = true;}
  11679. // scale the arrow
  11680. if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
  11681. // Added to support dashed lines
  11682. // David Jordan
  11683. // 2012-08-08
  11684. if (properties.dash) {
  11685. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  11686. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  11687. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  11688. }
  11689. if (properties.color !== undefined) {
  11690. if (util.isString(properties.color)) {
  11691. this.color.color = properties.color;
  11692. this.color.highlight = properties.color;
  11693. }
  11694. else {
  11695. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  11696. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  11697. if (properties.color.hover !== undefined) {this.color.hover = properties.color.hover;}
  11698. }
  11699. }
  11700. // A node is connected when it has a from and to node.
  11701. this.connect();
  11702. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  11703. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  11704. this.widthSelected = this.width * this.widthSelectionMultiplier;
  11705. // set draw method based on style
  11706. switch (this.style) {
  11707. case 'line': this.draw = this._drawLine; break;
  11708. case 'arrow': this.draw = this._drawArrow; break;
  11709. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  11710. case 'dash-line': this.draw = this._drawDashLine; break;
  11711. default: this.draw = this._drawLine; break;
  11712. }
  11713. };
  11714. /**
  11715. * Connect an edge to its nodes
  11716. */
  11717. Edge.prototype.connect = function () {
  11718. this.disconnect();
  11719. this.from = this.network.nodes[this.fromId] || null;
  11720. this.to = this.network.nodes[this.toId] || null;
  11721. this.connected = (this.from && this.to);
  11722. if (this.connected) {
  11723. this.from.attachEdge(this);
  11724. this.to.attachEdge(this);
  11725. }
  11726. else {
  11727. if (this.from) {
  11728. this.from.detachEdge(this);
  11729. }
  11730. if (this.to) {
  11731. this.to.detachEdge(this);
  11732. }
  11733. }
  11734. };
  11735. /**
  11736. * Disconnect an edge from its nodes
  11737. */
  11738. Edge.prototype.disconnect = function () {
  11739. if (this.from) {
  11740. this.from.detachEdge(this);
  11741. this.from = null;
  11742. }
  11743. if (this.to) {
  11744. this.to.detachEdge(this);
  11745. this.to = null;
  11746. }
  11747. this.connected = false;
  11748. };
  11749. /**
  11750. * get the title of this edge.
  11751. * @return {string} title The title of the edge, or undefined when no title
  11752. * has been set.
  11753. */
  11754. Edge.prototype.getTitle = function() {
  11755. return typeof this.title === "function" ? this.title() : this.title;
  11756. };
  11757. /**
  11758. * Retrieve the value of the edge. Can be undefined
  11759. * @return {Number} value
  11760. */
  11761. Edge.prototype.getValue = function() {
  11762. return this.value;
  11763. };
  11764. /**
  11765. * Adjust the value range of the edge. The edge will adjust it's width
  11766. * based on its value.
  11767. * @param {Number} min
  11768. * @param {Number} max
  11769. */
  11770. Edge.prototype.setValueRange = function(min, max) {
  11771. if (!this.widthFixed && this.value !== undefined) {
  11772. var scale = (this.widthMax - this.widthMin) / (max - min);
  11773. this.width = (this.value - min) * scale + this.widthMin;
  11774. }
  11775. };
  11776. /**
  11777. * Redraw a edge
  11778. * Draw this edge in the given canvas
  11779. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11780. * @param {CanvasRenderingContext2D} ctx
  11781. */
  11782. Edge.prototype.draw = function(ctx) {
  11783. throw "Method draw not initialized in edge";
  11784. };
  11785. /**
  11786. * Check if this object is overlapping with the provided object
  11787. * @param {Object} obj an object with parameters left, top
  11788. * @return {boolean} True if location is located on the edge
  11789. */
  11790. Edge.prototype.isOverlappingWith = function(obj) {
  11791. if (this.connected) {
  11792. var distMax = 10;
  11793. var xFrom = this.from.x;
  11794. var yFrom = this.from.y;
  11795. var xTo = this.to.x;
  11796. var yTo = this.to.y;
  11797. var xObj = obj.left;
  11798. var yObj = obj.top;
  11799. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  11800. return (dist < distMax);
  11801. }
  11802. else {
  11803. return false
  11804. }
  11805. };
  11806. /**
  11807. * Redraw a edge as a line
  11808. * Draw this edge in the given canvas
  11809. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11810. * @param {CanvasRenderingContext2D} ctx
  11811. * @private
  11812. */
  11813. Edge.prototype._drawLine = function(ctx) {
  11814. // set style
  11815. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  11816. else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
  11817. else {ctx.strokeStyle = this.color.color;}
  11818. ctx.lineWidth = this._getLineWidth();
  11819. if (this.from != this.to) {
  11820. // draw line
  11821. this._line(ctx);
  11822. // draw label
  11823. var point;
  11824. if (this.label) {
  11825. if (this.smooth == true) {
  11826. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  11827. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  11828. point = {x:midpointX, y:midpointY};
  11829. }
  11830. else {
  11831. point = this._pointOnLine(0.5);
  11832. }
  11833. this._label(ctx, this.label, point.x, point.y);
  11834. }
  11835. }
  11836. else {
  11837. var x, y;
  11838. var radius = this.length / 4;
  11839. var node = this.from;
  11840. if (!node.width) {
  11841. node.resize(ctx);
  11842. }
  11843. if (node.width > node.height) {
  11844. x = node.x + node.width / 2;
  11845. y = node.y - radius;
  11846. }
  11847. else {
  11848. x = node.x + radius;
  11849. y = node.y - node.height / 2;
  11850. }
  11851. this._circle(ctx, x, y, radius);
  11852. point = this._pointOnCircle(x, y, radius, 0.5);
  11853. this._label(ctx, this.label, point.x, point.y);
  11854. }
  11855. };
  11856. /**
  11857. * Get the line width of the edge. Depends on width and whether one of the
  11858. * connected nodes is selected.
  11859. * @return {Number} width
  11860. * @private
  11861. */
  11862. Edge.prototype._getLineWidth = function() {
  11863. if (this.selected == true) {
  11864. return Math.min(this.widthSelected, this.widthMax)*this.networkScaleInv;
  11865. }
  11866. else {
  11867. if (this.hover == true) {
  11868. return Math.min(this.hoverWidth, this.widthMax)*this.networkScaleInv;
  11869. }
  11870. else {
  11871. return this.width*this.networkScaleInv;
  11872. }
  11873. }
  11874. };
  11875. /**
  11876. * Draw a line between two nodes
  11877. * @param {CanvasRenderingContext2D} ctx
  11878. * @private
  11879. */
  11880. Edge.prototype._line = function (ctx) {
  11881. // draw a straight line
  11882. ctx.beginPath();
  11883. ctx.moveTo(this.from.x, this.from.y);
  11884. if (this.smooth == true) {
  11885. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  11886. }
  11887. else {
  11888. ctx.lineTo(this.to.x, this.to.y);
  11889. }
  11890. ctx.stroke();
  11891. };
  11892. /**
  11893. * Draw a line from a node to itself, a circle
  11894. * @param {CanvasRenderingContext2D} ctx
  11895. * @param {Number} x
  11896. * @param {Number} y
  11897. * @param {Number} radius
  11898. * @private
  11899. */
  11900. Edge.prototype._circle = function (ctx, x, y, radius) {
  11901. // draw a circle
  11902. ctx.beginPath();
  11903. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  11904. ctx.stroke();
  11905. };
  11906. /**
  11907. * Draw label with white background and with the middle at (x, y)
  11908. * @param {CanvasRenderingContext2D} ctx
  11909. * @param {String} text
  11910. * @param {Number} x
  11911. * @param {Number} y
  11912. * @private
  11913. */
  11914. Edge.prototype._label = function (ctx, text, x, y) {
  11915. if (text) {
  11916. // TODO: cache the calculated size
  11917. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  11918. this.fontSize + "px " + this.fontFace;
  11919. ctx.fillStyle = this.fontFill;
  11920. var width = ctx.measureText(text).width;
  11921. var height = this.fontSize;
  11922. var left = x - width / 2;
  11923. var top = y - height / 2;
  11924. ctx.fillRect(left, top, width, height);
  11925. // draw text
  11926. ctx.fillStyle = this.fontColor || "black";
  11927. ctx.textAlign = "left";
  11928. ctx.textBaseline = "top";
  11929. ctx.fillText(text, left, top);
  11930. }
  11931. };
  11932. /**
  11933. * Redraw a edge as a dashed line
  11934. * Draw this edge in the given canvas
  11935. * @author David Jordan
  11936. * @date 2012-08-08
  11937. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11938. * @param {CanvasRenderingContext2D} ctx
  11939. * @private
  11940. */
  11941. Edge.prototype._drawDashLine = function(ctx) {
  11942. // set style
  11943. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  11944. else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
  11945. else {ctx.strokeStyle = this.color.color;}
  11946. ctx.lineWidth = this._getLineWidth();
  11947. // only firefox and chrome support this method, else we use the legacy one.
  11948. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  11949. ctx.beginPath();
  11950. ctx.moveTo(this.from.x, this.from.y);
  11951. // configure the dash pattern
  11952. var pattern = [0];
  11953. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  11954. pattern = [this.dash.length,this.dash.gap];
  11955. }
  11956. else {
  11957. pattern = [5,5];
  11958. }
  11959. // set dash settings for chrome or firefox
  11960. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  11961. ctx.setLineDash(pattern);
  11962. ctx.lineDashOffset = 0;
  11963. } else { //Firefox
  11964. ctx.mozDash = pattern;
  11965. ctx.mozDashOffset = 0;
  11966. }
  11967. // draw the line
  11968. if (this.smooth == true) {
  11969. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  11970. }
  11971. else {
  11972. ctx.lineTo(this.to.x, this.to.y);
  11973. }
  11974. ctx.stroke();
  11975. // restore the dash settings.
  11976. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  11977. ctx.setLineDash([0]);
  11978. ctx.lineDashOffset = 0;
  11979. } else { //Firefox
  11980. ctx.mozDash = [0];
  11981. ctx.mozDashOffset = 0;
  11982. }
  11983. }
  11984. else { // unsupporting smooth lines
  11985. // draw dashed line
  11986. ctx.beginPath();
  11987. ctx.lineCap = 'round';
  11988. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  11989. {
  11990. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  11991. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  11992. }
  11993. 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
  11994. {
  11995. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  11996. [this.dash.length,this.dash.gap]);
  11997. }
  11998. else //If all else fails draw a line
  11999. {
  12000. ctx.moveTo(this.from.x, this.from.y);
  12001. ctx.lineTo(this.to.x, this.to.y);
  12002. }
  12003. ctx.stroke();
  12004. }
  12005. // draw label
  12006. if (this.label) {
  12007. var point;
  12008. if (this.smooth == true) {
  12009. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  12010. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  12011. point = {x:midpointX, y:midpointY};
  12012. }
  12013. else {
  12014. point = this._pointOnLine(0.5);
  12015. }
  12016. this._label(ctx, this.label, point.x, point.y);
  12017. }
  12018. };
  12019. /**
  12020. * Get a point on a line
  12021. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  12022. * @return {Object} point
  12023. * @private
  12024. */
  12025. Edge.prototype._pointOnLine = function (percentage) {
  12026. return {
  12027. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  12028. y: (1 - percentage) * this.from.y + percentage * this.to.y
  12029. }
  12030. };
  12031. /**
  12032. * Get a point on a circle
  12033. * @param {Number} x
  12034. * @param {Number} y
  12035. * @param {Number} radius
  12036. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  12037. * @return {Object} point
  12038. * @private
  12039. */
  12040. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  12041. var angle = (percentage - 3/8) * 2 * Math.PI;
  12042. return {
  12043. x: x + radius * Math.cos(angle),
  12044. y: y - radius * Math.sin(angle)
  12045. }
  12046. };
  12047. /**
  12048. * Redraw a edge as a line with an arrow halfway the line
  12049. * Draw this edge in the given canvas
  12050. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12051. * @param {CanvasRenderingContext2D} ctx
  12052. * @private
  12053. */
  12054. Edge.prototype._drawArrowCenter = function(ctx) {
  12055. var point;
  12056. // set style
  12057. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  12058. else if (this.hover == true) {ctx.strokeStyle = this.color.hover; ctx.fillStyle = this.color.hover;}
  12059. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  12060. ctx.lineWidth = this._getLineWidth();
  12061. if (this.from != this.to) {
  12062. // draw line
  12063. this._line(ctx);
  12064. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12065. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12066. // draw an arrow halfway the line
  12067. if (this.smooth == true) {
  12068. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  12069. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  12070. point = {x:midpointX, y:midpointY};
  12071. }
  12072. else {
  12073. point = this._pointOnLine(0.5);
  12074. }
  12075. ctx.arrow(point.x, point.y, angle, length);
  12076. ctx.fill();
  12077. ctx.stroke();
  12078. // draw label
  12079. if (this.label) {
  12080. this._label(ctx, this.label, point.x, point.y);
  12081. }
  12082. }
  12083. else {
  12084. // draw circle
  12085. var x, y;
  12086. var radius = 0.25 * Math.max(100,this.length);
  12087. var node = this.from;
  12088. if (!node.width) {
  12089. node.resize(ctx);
  12090. }
  12091. if (node.width > node.height) {
  12092. x = node.x + node.width * 0.5;
  12093. y = node.y - radius;
  12094. }
  12095. else {
  12096. x = node.x + radius;
  12097. y = node.y - node.height * 0.5;
  12098. }
  12099. this._circle(ctx, x, y, radius);
  12100. // draw all arrows
  12101. var angle = 0.2 * Math.PI;
  12102. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12103. point = this._pointOnCircle(x, y, radius, 0.5);
  12104. ctx.arrow(point.x, point.y, angle, length);
  12105. ctx.fill();
  12106. ctx.stroke();
  12107. // draw label
  12108. if (this.label) {
  12109. point = this._pointOnCircle(x, y, radius, 0.5);
  12110. this._label(ctx, this.label, point.x, point.y);
  12111. }
  12112. }
  12113. };
  12114. /**
  12115. * Redraw a edge as a line with an arrow
  12116. * Draw this edge in the given canvas
  12117. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12118. * @param {CanvasRenderingContext2D} ctx
  12119. * @private
  12120. */
  12121. Edge.prototype._drawArrow = function(ctx) {
  12122. // set style
  12123. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  12124. else if (this.hover == true) {ctx.strokeStyle = this.color.hover; ctx.fillStyle = this.color.hover;}
  12125. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  12126. ctx.lineWidth = this._getLineWidth();
  12127. var angle, length;
  12128. //draw a line
  12129. if (this.from != this.to) {
  12130. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12131. var dx = (this.to.x - this.from.x);
  12132. var dy = (this.to.y - this.from.y);
  12133. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12134. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  12135. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  12136. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  12137. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  12138. if (this.smooth == true) {
  12139. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  12140. dx = (this.to.x - this.via.x);
  12141. dy = (this.to.y - this.via.y);
  12142. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12143. }
  12144. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  12145. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  12146. var xTo,yTo;
  12147. if (this.smooth == true) {
  12148. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  12149. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  12150. }
  12151. else {
  12152. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  12153. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  12154. }
  12155. ctx.beginPath();
  12156. ctx.moveTo(xFrom,yFrom);
  12157. if (this.smooth == true) {
  12158. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  12159. }
  12160. else {
  12161. ctx.lineTo(xTo, yTo);
  12162. }
  12163. ctx.stroke();
  12164. // draw arrow at the end of the line
  12165. length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12166. ctx.arrow(xTo, yTo, angle, length);
  12167. ctx.fill();
  12168. ctx.stroke();
  12169. // draw label
  12170. if (this.label) {
  12171. var point;
  12172. if (this.smooth == true) {
  12173. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  12174. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  12175. point = {x:midpointX, y:midpointY};
  12176. }
  12177. else {
  12178. point = this._pointOnLine(0.5);
  12179. }
  12180. this._label(ctx, this.label, point.x, point.y);
  12181. }
  12182. }
  12183. else {
  12184. // draw circle
  12185. var node = this.from;
  12186. var x, y, arrow;
  12187. var radius = 0.25 * Math.max(100,this.length);
  12188. if (!node.width) {
  12189. node.resize(ctx);
  12190. }
  12191. if (node.width > node.height) {
  12192. x = node.x + node.width * 0.5;
  12193. y = node.y - radius;
  12194. arrow = {
  12195. x: x,
  12196. y: node.y,
  12197. angle: 0.9 * Math.PI
  12198. };
  12199. }
  12200. else {
  12201. x = node.x + radius;
  12202. y = node.y - node.height * 0.5;
  12203. arrow = {
  12204. x: node.x,
  12205. y: y,
  12206. angle: 0.6 * Math.PI
  12207. };
  12208. }
  12209. ctx.beginPath();
  12210. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  12211. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  12212. ctx.stroke();
  12213. // draw all arrows
  12214. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  12215. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  12216. ctx.fill();
  12217. ctx.stroke();
  12218. // draw label
  12219. if (this.label) {
  12220. point = this._pointOnCircle(x, y, radius, 0.5);
  12221. this._label(ctx, this.label, point.x, point.y);
  12222. }
  12223. }
  12224. };
  12225. /**
  12226. * Calculate the distance between a point (x3,y3) and a line segment from
  12227. * (x1,y1) to (x2,y2).
  12228. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  12229. * @param {number} x1
  12230. * @param {number} y1
  12231. * @param {number} x2
  12232. * @param {number} y2
  12233. * @param {number} x3
  12234. * @param {number} y3
  12235. * @private
  12236. */
  12237. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  12238. if (this.from != this.to) {
  12239. if (this.smooth == true) {
  12240. var minDistance = 1e9;
  12241. var i,t,x,y,dx,dy;
  12242. for (i = 0; i < 10; i++) {
  12243. t = 0.1*i;
  12244. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  12245. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  12246. dx = Math.abs(x3-x);
  12247. dy = Math.abs(y3-y);
  12248. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  12249. }
  12250. return minDistance
  12251. }
  12252. else {
  12253. var px = x2-x1,
  12254. py = y2-y1,
  12255. something = px*px + py*py,
  12256. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  12257. if (u > 1) {
  12258. u = 1;
  12259. }
  12260. else if (u < 0) {
  12261. u = 0;
  12262. }
  12263. var x = x1 + u * px,
  12264. y = y1 + u * py,
  12265. dx = x - x3,
  12266. dy = y - y3;
  12267. //# Note: If the actual distance does not matter,
  12268. //# if you only want to compare what this function
  12269. //# returns to other results of this function, you
  12270. //# can just return the squared distance instead
  12271. //# (i.e. remove the sqrt) to gain a little performance
  12272. return Math.sqrt(dx*dx + dy*dy);
  12273. }
  12274. }
  12275. else {
  12276. var x, y, dx, dy;
  12277. var radius = this.length / 4;
  12278. var node = this.from;
  12279. if (!node.width) {
  12280. node.resize(ctx);
  12281. }
  12282. if (node.width > node.height) {
  12283. x = node.x + node.width / 2;
  12284. y = node.y - radius;
  12285. }
  12286. else {
  12287. x = node.x + radius;
  12288. y = node.y - node.height / 2;
  12289. }
  12290. dx = x - x3;
  12291. dy = y - y3;
  12292. return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
  12293. }
  12294. };
  12295. /**
  12296. * This allows the zoom level of the network to influence the rendering
  12297. *
  12298. * @param scale
  12299. */
  12300. Edge.prototype.setScale = function(scale) {
  12301. this.networkScaleInv = 1.0/scale;
  12302. };
  12303. Edge.prototype.select = function() {
  12304. this.selected = true;
  12305. };
  12306. Edge.prototype.unselect = function() {
  12307. this.selected = false;
  12308. };
  12309. Edge.prototype.positionBezierNode = function() {
  12310. if (this.via !== null) {
  12311. this.via.x = 0.5 * (this.from.x + this.to.x);
  12312. this.via.y = 0.5 * (this.from.y + this.to.y);
  12313. }
  12314. };
  12315. /**
  12316. * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true.
  12317. * @param ctx
  12318. */
  12319. Edge.prototype._drawControlNodes = function(ctx) {
  12320. if (this.controlNodesEnabled == true) {
  12321. if (this.controlNodes.from === null && this.controlNodes.to === null) {
  12322. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  12323. var nodeIdTo = "edgeIdTo:".concat(this.id);
  12324. var constants = {
  12325. nodes:{group:'', radius:8},
  12326. physics:{damping:0},
  12327. clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
  12328. };
  12329. this.controlNodes.from = new Node(
  12330. {id:nodeIdFrom,
  12331. shape:'dot',
  12332. color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
  12333. },{},{},constants);
  12334. this.controlNodes.to = new Node(
  12335. {id:nodeIdTo,
  12336. shape:'dot',
  12337. color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
  12338. },{},{},constants);
  12339. }
  12340. if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
  12341. this.controlNodes.positions = this.getControlNodePositions(ctx);
  12342. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  12343. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  12344. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  12345. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  12346. }
  12347. this.controlNodes.from.draw(ctx);
  12348. this.controlNodes.to.draw(ctx);
  12349. }
  12350. else {
  12351. this.controlNodes = {from:null, to:null, positions:{}};
  12352. }
  12353. }
  12354. /**
  12355. * Enable control nodes.
  12356. * @private
  12357. */
  12358. Edge.prototype._enableControlNodes = function() {
  12359. this.controlNodesEnabled = true;
  12360. }
  12361. /**
  12362. * disable control nodes
  12363. * @private
  12364. */
  12365. Edge.prototype._disableControlNodes = function() {
  12366. this.controlNodesEnabled = false;
  12367. }
  12368. /**
  12369. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
  12370. * @param x
  12371. * @param y
  12372. * @returns {null}
  12373. * @private
  12374. */
  12375. Edge.prototype._getSelectedControlNode = function(x,y) {
  12376. var positions = this.controlNodes.positions;
  12377. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
  12378. var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
  12379. if (fromDistance < 15) {
  12380. this.connectedNode = this.from;
  12381. this.from = this.controlNodes.from;
  12382. return this.controlNodes.from;
  12383. }
  12384. else if (toDistance < 15) {
  12385. this.connectedNode = this.to;
  12386. this.to = this.controlNodes.to;
  12387. return this.controlNodes.to;
  12388. }
  12389. else {
  12390. return null;
  12391. }
  12392. }
  12393. /**
  12394. * this resets the control nodes to their original position.
  12395. * @private
  12396. */
  12397. Edge.prototype._restoreControlNodes = function() {
  12398. if (this.controlNodes.from.selected == true) {
  12399. this.from = this.connectedNode;
  12400. this.connectedNode = null;
  12401. this.controlNodes.from.unselect();
  12402. }
  12403. if (this.controlNodes.to.selected == true) {
  12404. this.to = this.connectedNode;
  12405. this.connectedNode = null;
  12406. this.controlNodes.to.unselect();
  12407. }
  12408. }
  12409. /**
  12410. * this calculates the position of the control nodes on the edges of the parent nodes.
  12411. *
  12412. * @param ctx
  12413. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  12414. */
  12415. Edge.prototype.getControlNodePositions = function(ctx) {
  12416. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12417. var dx = (this.to.x - this.from.x);
  12418. var dy = (this.to.y - this.from.y);
  12419. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12420. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  12421. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  12422. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  12423. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  12424. if (this.smooth == true) {
  12425. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  12426. dx = (this.to.x - this.via.x);
  12427. dy = (this.to.y - this.via.y);
  12428. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  12429. }
  12430. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  12431. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  12432. var xTo,yTo;
  12433. if (this.smooth == true) {
  12434. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  12435. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  12436. }
  12437. else {
  12438. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  12439. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  12440. }
  12441. return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
  12442. }
  12443. /**
  12444. * Popup is a class to create a popup window with some text
  12445. * @param {Element} container The container object.
  12446. * @param {Number} [x]
  12447. * @param {Number} [y]
  12448. * @param {String} [text]
  12449. * @param {Object} [style] An object containing borderColor,
  12450. * backgroundColor, etc.
  12451. */
  12452. function Popup(container, x, y, text, style) {
  12453. if (container) {
  12454. this.container = container;
  12455. }
  12456. else {
  12457. this.container = document.body;
  12458. }
  12459. // x, y and text are optional, see if a style object was passed in their place
  12460. if (style === undefined) {
  12461. if (typeof x === "object") {
  12462. style = x;
  12463. x = undefined;
  12464. } else if (typeof text === "object") {
  12465. style = text;
  12466. text = undefined;
  12467. } else {
  12468. // for backwards compatibility, in case clients other than Network are creating Popup directly
  12469. style = {
  12470. fontColor: 'black',
  12471. fontSize: 14, // px
  12472. fontFace: 'verdana',
  12473. color: {
  12474. border: '#666',
  12475. background: '#FFFFC6'
  12476. }
  12477. }
  12478. }
  12479. }
  12480. this.x = 0;
  12481. this.y = 0;
  12482. this.padding = 5;
  12483. if (x !== undefined && y !== undefined ) {
  12484. this.setPosition(x, y);
  12485. }
  12486. if (text !== undefined) {
  12487. this.setText(text);
  12488. }
  12489. // create the frame
  12490. this.frame = document.createElement("div");
  12491. var styleAttr = this.frame.style;
  12492. styleAttr.position = "absolute";
  12493. styleAttr.visibility = "hidden";
  12494. styleAttr.border = "1px solid " + style.color.border;
  12495. styleAttr.color = style.fontColor;
  12496. styleAttr.fontSize = style.fontSize + "px";
  12497. styleAttr.fontFamily = style.fontFace;
  12498. styleAttr.padding = this.padding + "px";
  12499. styleAttr.backgroundColor = style.color.background;
  12500. styleAttr.borderRadius = "3px";
  12501. styleAttr.MozBorderRadius = "3px";
  12502. styleAttr.WebkitBorderRadius = "3px";
  12503. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  12504. styleAttr.whiteSpace = "nowrap";
  12505. this.container.appendChild(this.frame);
  12506. }
  12507. /**
  12508. * @param {number} x Horizontal position of the popup window
  12509. * @param {number} y Vertical position of the popup window
  12510. */
  12511. Popup.prototype.setPosition = function(x, y) {
  12512. this.x = parseInt(x);
  12513. this.y = parseInt(y);
  12514. };
  12515. /**
  12516. * Set the text for the popup window. This can be HTML code
  12517. * @param {string} text
  12518. */
  12519. Popup.prototype.setText = function(text) {
  12520. this.frame.innerHTML = text;
  12521. };
  12522. /**
  12523. * Show the popup window
  12524. * @param {boolean} show Optional. Show or hide the window
  12525. */
  12526. Popup.prototype.show = function (show) {
  12527. if (show === undefined) {
  12528. show = true;
  12529. }
  12530. if (show) {
  12531. var height = this.frame.clientHeight;
  12532. var width = this.frame.clientWidth;
  12533. var maxHeight = this.frame.parentNode.clientHeight;
  12534. var maxWidth = this.frame.parentNode.clientWidth;
  12535. var top = (this.y - height);
  12536. if (top + height + this.padding > maxHeight) {
  12537. top = maxHeight - height - this.padding;
  12538. }
  12539. if (top < this.padding) {
  12540. top = this.padding;
  12541. }
  12542. var left = this.x;
  12543. if (left + width + this.padding > maxWidth) {
  12544. left = maxWidth - width - this.padding;
  12545. }
  12546. if (left < this.padding) {
  12547. left = this.padding;
  12548. }
  12549. this.frame.style.left = left + "px";
  12550. this.frame.style.top = top + "px";
  12551. this.frame.style.visibility = "visible";
  12552. }
  12553. else {
  12554. this.hide();
  12555. }
  12556. };
  12557. /**
  12558. * Hide the popup window
  12559. */
  12560. Popup.prototype.hide = function () {
  12561. this.frame.style.visibility = "hidden";
  12562. };
  12563. /**
  12564. * @class Groups
  12565. * This class can store groups and properties specific for groups.
  12566. */
  12567. function Groups() {
  12568. this.clear();
  12569. this.defaultIndex = 0;
  12570. }
  12571. /**
  12572. * default constants for group colors
  12573. */
  12574. Groups.DEFAULT = [
  12575. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  12576. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  12577. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  12578. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  12579. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  12580. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  12581. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  12582. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  12583. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  12584. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  12585. ];
  12586. /**
  12587. * Clear all groups
  12588. */
  12589. Groups.prototype.clear = function () {
  12590. this.groups = {};
  12591. this.groups.length = function()
  12592. {
  12593. var i = 0;
  12594. for ( var p in this ) {
  12595. if (this.hasOwnProperty(p)) {
  12596. i++;
  12597. }
  12598. }
  12599. return i;
  12600. }
  12601. };
  12602. /**
  12603. * get group properties of a groupname. If groupname is not found, a new group
  12604. * is added.
  12605. * @param {*} groupname Can be a number, string, Date, etc.
  12606. * @return {Object} group The created group, containing all group properties
  12607. */
  12608. Groups.prototype.get = function (groupname) {
  12609. var group = this.groups[groupname];
  12610. if (group == undefined) {
  12611. // create new group
  12612. var index = this.defaultIndex % Groups.DEFAULT.length;
  12613. this.defaultIndex++;
  12614. group = {};
  12615. group.color = Groups.DEFAULT[index];
  12616. this.groups[groupname] = group;
  12617. }
  12618. return group;
  12619. };
  12620. /**
  12621. * Add a custom group style
  12622. * @param {String} groupname
  12623. * @param {Object} style An object containing borderColor,
  12624. * backgroundColor, etc.
  12625. * @return {Object} group The created group object
  12626. */
  12627. Groups.prototype.add = function (groupname, style) {
  12628. this.groups[groupname] = style;
  12629. if (style.color) {
  12630. style.color = util.parseColor(style.color);
  12631. }
  12632. return style;
  12633. };
  12634. /**
  12635. * @class Images
  12636. * This class loads images and keeps them stored.
  12637. */
  12638. function Images() {
  12639. this.images = {};
  12640. this.callback = undefined;
  12641. }
  12642. /**
  12643. * Set an onload callback function. This will be called each time an image
  12644. * is loaded
  12645. * @param {function} callback
  12646. */
  12647. Images.prototype.setOnloadCallback = function(callback) {
  12648. this.callback = callback;
  12649. };
  12650. /**
  12651. *
  12652. * @param {string} url Url of the image
  12653. * @return {Image} img The image object
  12654. */
  12655. Images.prototype.load = function(url) {
  12656. var img = this.images[url];
  12657. if (img == undefined) {
  12658. // create the image
  12659. var images = this;
  12660. img = new Image();
  12661. this.images[url] = img;
  12662. img.onload = function() {
  12663. if (images.callback) {
  12664. images.callback(this);
  12665. }
  12666. };
  12667. img.src = url;
  12668. }
  12669. return img;
  12670. };
  12671. /**
  12672. * Created by Alex on 2/6/14.
  12673. */
  12674. var physicsMixin = {
  12675. /**
  12676. * Toggling barnes Hut calculation on and off.
  12677. *
  12678. * @private
  12679. */
  12680. _toggleBarnesHut: function () {
  12681. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  12682. this._loadSelectedForceSolver();
  12683. this.moving = true;
  12684. this.start();
  12685. },
  12686. /**
  12687. * This loads the node force solver based on the barnes hut or repulsion algorithm
  12688. *
  12689. * @private
  12690. */
  12691. _loadSelectedForceSolver: function () {
  12692. // this overloads the this._calculateNodeForces
  12693. if (this.constants.physics.barnesHut.enabled == true) {
  12694. this._clearMixin(repulsionMixin);
  12695. this._clearMixin(hierarchalRepulsionMixin);
  12696. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  12697. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  12698. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  12699. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  12700. this._loadMixin(barnesHutMixin);
  12701. }
  12702. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  12703. this._clearMixin(barnesHutMixin);
  12704. this._clearMixin(repulsionMixin);
  12705. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  12706. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  12707. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  12708. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  12709. this._loadMixin(hierarchalRepulsionMixin);
  12710. }
  12711. else {
  12712. this._clearMixin(barnesHutMixin);
  12713. this._clearMixin(hierarchalRepulsionMixin);
  12714. this.barnesHutTree = undefined;
  12715. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  12716. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  12717. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  12718. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  12719. this._loadMixin(repulsionMixin);
  12720. }
  12721. },
  12722. /**
  12723. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  12724. * if there is more than one node. If it is just one node, we dont calculate anything.
  12725. *
  12726. * @private
  12727. */
  12728. _initializeForceCalculation: function () {
  12729. // stop calculation if there is only one node
  12730. if (this.nodeIndices.length == 1) {
  12731. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  12732. }
  12733. else {
  12734. // if there are too many nodes on screen, we cluster without repositioning
  12735. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  12736. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  12737. }
  12738. // we now start the force calculation
  12739. this._calculateForces();
  12740. }
  12741. },
  12742. /**
  12743. * Calculate the external forces acting on the nodes
  12744. * Forces are caused by: edges, repulsing forces between nodes, gravity
  12745. * @private
  12746. */
  12747. _calculateForces: function () {
  12748. // Gravity is required to keep separated groups from floating off
  12749. // the forces are reset to zero in this loop by using _setForce instead
  12750. // of _addForce
  12751. this._calculateGravitationalForces();
  12752. this._calculateNodeForces();
  12753. if (this.constants.smoothCurves == true) {
  12754. this._calculateSpringForcesWithSupport();
  12755. }
  12756. else {
  12757. if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  12758. this._calculateHierarchicalSpringForces();
  12759. }
  12760. else {
  12761. this._calculateSpringForces();
  12762. }
  12763. }
  12764. },
  12765. /**
  12766. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  12767. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  12768. * This function joins the datanodes and invisible (called support) nodes into one object.
  12769. * We do this so we do not contaminate this.nodes with the support nodes.
  12770. *
  12771. * @private
  12772. */
  12773. _updateCalculationNodes: function () {
  12774. if (this.constants.smoothCurves == true) {
  12775. this.calculationNodes = {};
  12776. this.calculationNodeIndices = [];
  12777. for (var nodeId in this.nodes) {
  12778. if (this.nodes.hasOwnProperty(nodeId)) {
  12779. this.calculationNodes[nodeId] = this.nodes[nodeId];
  12780. }
  12781. }
  12782. var supportNodes = this.sectors['support']['nodes'];
  12783. for (var supportNodeId in supportNodes) {
  12784. if (supportNodes.hasOwnProperty(supportNodeId)) {
  12785. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  12786. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  12787. }
  12788. else {
  12789. supportNodes[supportNodeId]._setForce(0, 0);
  12790. }
  12791. }
  12792. }
  12793. for (var idx in this.calculationNodes) {
  12794. if (this.calculationNodes.hasOwnProperty(idx)) {
  12795. this.calculationNodeIndices.push(idx);
  12796. }
  12797. }
  12798. }
  12799. else {
  12800. this.calculationNodes = this.nodes;
  12801. this.calculationNodeIndices = this.nodeIndices;
  12802. }
  12803. },
  12804. /**
  12805. * this function applies the central gravity effect to keep groups from floating off
  12806. *
  12807. * @private
  12808. */
  12809. _calculateGravitationalForces: function () {
  12810. var dx, dy, distance, node, i;
  12811. var nodes = this.calculationNodes;
  12812. var gravity = this.constants.physics.centralGravity;
  12813. var gravityForce = 0;
  12814. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  12815. node = nodes[this.calculationNodeIndices[i]];
  12816. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  12817. // gravity does not apply when we are in a pocket sector
  12818. if (this._sector() == "default" && gravity != 0) {
  12819. dx = -node.x;
  12820. dy = -node.y;
  12821. distance = Math.sqrt(dx * dx + dy * dy);
  12822. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  12823. node.fx = dx * gravityForce;
  12824. node.fy = dy * gravityForce;
  12825. }
  12826. else {
  12827. node.fx = 0;
  12828. node.fy = 0;
  12829. }
  12830. }
  12831. },
  12832. /**
  12833. * this function calculates the effects of the springs in the case of unsmooth curves.
  12834. *
  12835. * @private
  12836. */
  12837. _calculateSpringForces: function () {
  12838. var edgeLength, edge, edgeId;
  12839. var dx, dy, fx, fy, springForce, distance;
  12840. var edges = this.edges;
  12841. // forces caused by the edges, modelled as springs
  12842. for (edgeId in edges) {
  12843. if (edges.hasOwnProperty(edgeId)) {
  12844. edge = edges[edgeId];
  12845. if (edge.connected) {
  12846. // only calculate forces if nodes are in the same sector
  12847. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  12848. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  12849. // this implies that the edges between big clusters are longer
  12850. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  12851. dx = (edge.from.x - edge.to.x);
  12852. dy = (edge.from.y - edge.to.y);
  12853. distance = Math.sqrt(dx * dx + dy * dy);
  12854. if (distance == 0) {
  12855. distance = 0.01;
  12856. }
  12857. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  12858. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  12859. fx = dx * springForce;
  12860. fy = dy * springForce;
  12861. edge.from.fx += fx;
  12862. edge.from.fy += fy;
  12863. edge.to.fx -= fx;
  12864. edge.to.fy -= fy;
  12865. }
  12866. }
  12867. }
  12868. }
  12869. },
  12870. /**
  12871. * This function calculates the springforces on the nodes, accounting for the support nodes.
  12872. *
  12873. * @private
  12874. */
  12875. _calculateSpringForcesWithSupport: function () {
  12876. var edgeLength, edge, edgeId, combinedClusterSize;
  12877. var edges = this.edges;
  12878. // forces caused by the edges, modelled as springs
  12879. for (edgeId in edges) {
  12880. if (edges.hasOwnProperty(edgeId)) {
  12881. edge = edges[edgeId];
  12882. if (edge.connected) {
  12883. // only calculate forces if nodes are in the same sector
  12884. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  12885. if (edge.via != null) {
  12886. var node1 = edge.to;
  12887. var node2 = edge.via;
  12888. var node3 = edge.from;
  12889. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  12890. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  12891. // this implies that the edges between big clusters are longer
  12892. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  12893. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  12894. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  12895. }
  12896. }
  12897. }
  12898. }
  12899. }
  12900. },
  12901. /**
  12902. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  12903. *
  12904. * @param node1
  12905. * @param node2
  12906. * @param edgeLength
  12907. * @private
  12908. */
  12909. _calculateSpringForce: function (node1, node2, edgeLength) {
  12910. var dx, dy, fx, fy, springForce, distance;
  12911. dx = (node1.x - node2.x);
  12912. dy = (node1.y - node2.y);
  12913. distance = Math.sqrt(dx * dx + dy * dy);
  12914. if (distance == 0) {
  12915. distance = 0.01;
  12916. }
  12917. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  12918. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  12919. fx = dx * springForce;
  12920. fy = dy * springForce;
  12921. node1.fx += fx;
  12922. node1.fy += fy;
  12923. node2.fx -= fx;
  12924. node2.fy -= fy;
  12925. },
  12926. /**
  12927. * Load the HTML for the physics config and bind it
  12928. * @private
  12929. */
  12930. _loadPhysicsConfiguration: function () {
  12931. if (this.physicsConfiguration === undefined) {
  12932. this.backupConstants = {};
  12933. util.deepExtend(this.backupConstants,this.constants);
  12934. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  12935. this.physicsConfiguration = document.createElement('div');
  12936. this.physicsConfiguration.className = "PhysicsConfiguration";
  12937. this.physicsConfiguration.innerHTML = '' +
  12938. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  12939. '<tr>' +
  12940. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  12941. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  12942. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  12943. '</tr>' +
  12944. '</table>' +
  12945. '<table id="graph_BH_table" style="display:none">' +
  12946. '<tr><td><b>Barnes Hut</b></td></tr>' +
  12947. '<tr>' +
  12948. '<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>' +
  12949. '</tr>' +
  12950. '<tr>' +
  12951. '<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>' +
  12952. '</tr>' +
  12953. '<tr>' +
  12954. '<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>' +
  12955. '</tr>' +
  12956. '<tr>' +
  12957. '<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>' +
  12958. '</tr>' +
  12959. '<tr>' +
  12960. '<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>' +
  12961. '</tr>' +
  12962. '</table>' +
  12963. '<table id="graph_R_table" style="display:none">' +
  12964. '<tr><td><b>Repulsion</b></td></tr>' +
  12965. '<tr>' +
  12966. '<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>' +
  12967. '</tr>' +
  12968. '<tr>' +
  12969. '<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>' +
  12970. '</tr>' +
  12971. '<tr>' +
  12972. '<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>' +
  12973. '</tr>' +
  12974. '<tr>' +
  12975. '<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>' +
  12976. '</tr>' +
  12977. '<tr>' +
  12978. '<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>' +
  12979. '</tr>' +
  12980. '</table>' +
  12981. '<table id="graph_H_table" style="display:none">' +
  12982. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  12983. '<tr>' +
  12984. '<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>' +
  12985. '</tr>' +
  12986. '<tr>' +
  12987. '<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>' +
  12988. '</tr>' +
  12989. '<tr>' +
  12990. '<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>' +
  12991. '</tr>' +
  12992. '<tr>' +
  12993. '<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>' +
  12994. '</tr>' +
  12995. '<tr>' +
  12996. '<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>' +
  12997. '</tr>' +
  12998. '<tr>' +
  12999. '<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>' +
  13000. '</tr>' +
  13001. '<tr>' +
  13002. '<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>' +
  13003. '</tr>' +
  13004. '<tr>' +
  13005. '<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>' +
  13006. '</tr>' +
  13007. '</table>' +
  13008. '<table><tr><td><b>Options:</b></td></tr>' +
  13009. '<tr>' +
  13010. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  13011. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  13012. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  13013. '</tr>' +
  13014. '</table>'
  13015. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  13016. this.optionsDiv = document.createElement("div");
  13017. this.optionsDiv.style.fontSize = "14px";
  13018. this.optionsDiv.style.fontFamily = "verdana";
  13019. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  13020. var rangeElement;
  13021. rangeElement = document.getElementById('graph_BH_gc');
  13022. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  13023. rangeElement = document.getElementById('graph_BH_cg');
  13024. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  13025. rangeElement = document.getElementById('graph_BH_sc');
  13026. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  13027. rangeElement = document.getElementById('graph_BH_sl');
  13028. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  13029. rangeElement = document.getElementById('graph_BH_damp');
  13030. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  13031. rangeElement = document.getElementById('graph_R_nd');
  13032. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  13033. rangeElement = document.getElementById('graph_R_cg');
  13034. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  13035. rangeElement = document.getElementById('graph_R_sc');
  13036. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  13037. rangeElement = document.getElementById('graph_R_sl');
  13038. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  13039. rangeElement = document.getElementById('graph_R_damp');
  13040. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  13041. rangeElement = document.getElementById('graph_H_nd');
  13042. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  13043. rangeElement = document.getElementById('graph_H_cg');
  13044. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  13045. rangeElement = document.getElementById('graph_H_sc');
  13046. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  13047. rangeElement = document.getElementById('graph_H_sl');
  13048. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  13049. rangeElement = document.getElementById('graph_H_damp');
  13050. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  13051. rangeElement = document.getElementById('graph_H_direction');
  13052. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  13053. rangeElement = document.getElementById('graph_H_levsep');
  13054. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  13055. rangeElement = document.getElementById('graph_H_nspac');
  13056. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  13057. var radioButton1 = document.getElementById("graph_physicsMethod1");
  13058. var radioButton2 = document.getElementById("graph_physicsMethod2");
  13059. var radioButton3 = document.getElementById("graph_physicsMethod3");
  13060. radioButton2.checked = true;
  13061. if (this.constants.physics.barnesHut.enabled) {
  13062. radioButton1.checked = true;
  13063. }
  13064. if (this.constants.hierarchicalLayout.enabled) {
  13065. radioButton3.checked = true;
  13066. }
  13067. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  13068. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  13069. var graph_generateOptions = document.getElementById("graph_generateOptions");
  13070. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  13071. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  13072. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  13073. if (this.constants.smoothCurves == true) {
  13074. graph_toggleSmooth.style.background = "#A4FF56";
  13075. }
  13076. else {
  13077. graph_toggleSmooth.style.background = "#FF8532";
  13078. }
  13079. switchConfigurations.apply(this);
  13080. radioButton1.onchange = switchConfigurations.bind(this);
  13081. radioButton2.onchange = switchConfigurations.bind(this);
  13082. radioButton3.onchange = switchConfigurations.bind(this);
  13083. }
  13084. },
  13085. /**
  13086. * This overwrites the this.constants.
  13087. *
  13088. * @param constantsVariableName
  13089. * @param value
  13090. * @private
  13091. */
  13092. _overWriteGraphConstants: function (constantsVariableName, value) {
  13093. var nameArray = constantsVariableName.split("_");
  13094. if (nameArray.length == 1) {
  13095. this.constants[nameArray[0]] = value;
  13096. }
  13097. else if (nameArray.length == 2) {
  13098. this.constants[nameArray[0]][nameArray[1]] = value;
  13099. }
  13100. else if (nameArray.length == 3) {
  13101. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  13102. }
  13103. }
  13104. };
  13105. /**
  13106. * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
  13107. */
  13108. function graphToggleSmoothCurves () {
  13109. this.constants.smoothCurves = !this.constants.smoothCurves;
  13110. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  13111. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  13112. else {graph_toggleSmooth.style.background = "#FF8532";}
  13113. this._configureSmoothCurves(false);
  13114. };
  13115. /**
  13116. * this function is used to scramble the nodes
  13117. *
  13118. */
  13119. function graphRepositionNodes () {
  13120. for (var nodeId in this.calculationNodes) {
  13121. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  13122. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  13123. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  13124. }
  13125. }
  13126. if (this.constants.hierarchicalLayout.enabled == true) {
  13127. this._setupHierarchicalLayout();
  13128. }
  13129. else {
  13130. this.repositionNodes();
  13131. }
  13132. this.moving = true;
  13133. this.start();
  13134. };
  13135. /**
  13136. * this is used to generate an options file from the playing with physics system.
  13137. */
  13138. function graphGenerateOptions () {
  13139. var options = "No options are required, default values used.";
  13140. var optionsSpecific = [];
  13141. var radioButton1 = document.getElementById("graph_physicsMethod1");
  13142. var radioButton2 = document.getElementById("graph_physicsMethod2");
  13143. if (radioButton1.checked == true) {
  13144. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  13145. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  13146. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  13147. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  13148. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  13149. if (optionsSpecific.length != 0) {
  13150. options = "var options = {";
  13151. options += "physics: {barnesHut: {";
  13152. for (var i = 0; i < optionsSpecific.length; i++) {
  13153. options += optionsSpecific[i];
  13154. if (i < optionsSpecific.length - 1) {
  13155. options += ", "
  13156. }
  13157. }
  13158. options += '}}'
  13159. }
  13160. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  13161. if (optionsSpecific.length == 0) {options = "var options = {";}
  13162. else {options += ", "}
  13163. options += "smoothCurves: " + this.constants.smoothCurves;
  13164. }
  13165. if (options != "No options are required, default values used.") {
  13166. options += '};'
  13167. }
  13168. }
  13169. else if (radioButton2.checked == true) {
  13170. options = "var options = {";
  13171. options += "physics: {barnesHut: {enabled: false}";
  13172. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  13173. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  13174. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  13175. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  13176. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  13177. if (optionsSpecific.length != 0) {
  13178. options += ", repulsion: {";
  13179. for (var i = 0; i < optionsSpecific.length; i++) {
  13180. options += optionsSpecific[i];
  13181. if (i < optionsSpecific.length - 1) {
  13182. options += ", "
  13183. }
  13184. }
  13185. options += '}}'
  13186. }
  13187. if (optionsSpecific.length == 0) {options += "}"}
  13188. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  13189. options += ", smoothCurves: " + this.constants.smoothCurves;
  13190. }
  13191. options += '};'
  13192. }
  13193. else {
  13194. options = "var options = {";
  13195. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  13196. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  13197. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  13198. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  13199. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  13200. if (optionsSpecific.length != 0) {
  13201. options += "physics: {hierarchicalRepulsion: {";
  13202. for (var i = 0; i < optionsSpecific.length; i++) {
  13203. options += optionsSpecific[i];
  13204. if (i < optionsSpecific.length - 1) {
  13205. options += ", ";
  13206. }
  13207. }
  13208. options += '}},';
  13209. }
  13210. options += 'hierarchicalLayout: {';
  13211. optionsSpecific = [];
  13212. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  13213. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  13214. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  13215. if (optionsSpecific.length != 0) {
  13216. for (var i = 0; i < optionsSpecific.length; i++) {
  13217. options += optionsSpecific[i];
  13218. if (i < optionsSpecific.length - 1) {
  13219. options += ", "
  13220. }
  13221. }
  13222. options += '}'
  13223. }
  13224. else {
  13225. options += "enabled:true}";
  13226. }
  13227. options += '};'
  13228. }
  13229. this.optionsDiv.innerHTML = options;
  13230. };
  13231. /**
  13232. * this is used to switch between barnesHut, repulsion and hierarchical.
  13233. *
  13234. */
  13235. function switchConfigurations () {
  13236. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  13237. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  13238. var tableId = "graph_" + radioButton + "_table";
  13239. var table = document.getElementById(tableId);
  13240. table.style.display = "block";
  13241. for (var i = 0; i < ids.length; i++) {
  13242. if (ids[i] != tableId) {
  13243. table = document.getElementById(ids[i]);
  13244. table.style.display = "none";
  13245. }
  13246. }
  13247. this._restoreNodes();
  13248. if (radioButton == "R") {
  13249. this.constants.hierarchicalLayout.enabled = false;
  13250. this.constants.physics.hierarchicalRepulsion.enabled = false;
  13251. this.constants.physics.barnesHut.enabled = false;
  13252. }
  13253. else if (radioButton == "H") {
  13254. if (this.constants.hierarchicalLayout.enabled == false) {
  13255. this.constants.hierarchicalLayout.enabled = true;
  13256. this.constants.physics.hierarchicalRepulsion.enabled = true;
  13257. this.constants.physics.barnesHut.enabled = false;
  13258. this._setupHierarchicalLayout();
  13259. }
  13260. }
  13261. else {
  13262. this.constants.hierarchicalLayout.enabled = false;
  13263. this.constants.physics.hierarchicalRepulsion.enabled = false;
  13264. this.constants.physics.barnesHut.enabled = true;
  13265. }
  13266. this._loadSelectedForceSolver();
  13267. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  13268. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  13269. else {graph_toggleSmooth.style.background = "#FF8532";}
  13270. this.moving = true;
  13271. this.start();
  13272. }
  13273. /**
  13274. * this generates the ranges depending on the iniital values.
  13275. *
  13276. * @param id
  13277. * @param map
  13278. * @param constantsVariableName
  13279. */
  13280. function showValueOfRange (id,map,constantsVariableName) {
  13281. var valueId = id + "_value";
  13282. var rangeValue = document.getElementById(id).value;
  13283. if (map instanceof Array) {
  13284. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  13285. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  13286. }
  13287. else {
  13288. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  13289. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  13290. }
  13291. if (constantsVariableName == "hierarchicalLayout_direction" ||
  13292. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  13293. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  13294. this._setupHierarchicalLayout();
  13295. }
  13296. this.moving = true;
  13297. this.start();
  13298. };
  13299. /**
  13300. * Created by Alex on 2/10/14.
  13301. */
  13302. var hierarchalRepulsionMixin = {
  13303. /**
  13304. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  13305. * This field is linearly approximated.
  13306. *
  13307. * @private
  13308. */
  13309. _calculateNodeForces: function () {
  13310. var dx, dy, distance, fx, fy, combinedClusterSize,
  13311. repulsingForce, node1, node2, i, j;
  13312. var nodes = this.calculationNodes;
  13313. var nodeIndices = this.calculationNodeIndices;
  13314. // approximation constants
  13315. var b = 5;
  13316. var a_base = 0.5 * -b;
  13317. // repulsing forces between nodes
  13318. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  13319. var minimumDistance = nodeDistance;
  13320. var a = a_base / minimumDistance;
  13321. // we loop from i over all but the last entree in the array
  13322. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  13323. for (i = 0; i < nodeIndices.length - 1; i++) {
  13324. node1 = nodes[nodeIndices[i]];
  13325. for (j = i + 1; j < nodeIndices.length; j++) {
  13326. node2 = nodes[nodeIndices[j]];
  13327. if (node1.level == node2.level) {
  13328. dx = node2.x - node1.x;
  13329. dy = node2.y - node1.y;
  13330. distance = Math.sqrt(dx * dx + dy * dy);
  13331. if (distance < 2 * minimumDistance) {
  13332. repulsingForce = a * distance + b;
  13333. var c = 0.05;
  13334. var d = 2 * minimumDistance * 2 * c;
  13335. repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c);
  13336. // normalize force with
  13337. if (distance == 0) {
  13338. distance = 0.01;
  13339. }
  13340. else {
  13341. repulsingForce = repulsingForce / distance;
  13342. }
  13343. fx = dx * repulsingForce;
  13344. fy = dy * repulsingForce;
  13345. node1.fx -= fx;
  13346. node1.fy -= fy;
  13347. node2.fx += fx;
  13348. node2.fy += fy;
  13349. }
  13350. }
  13351. }
  13352. }
  13353. },
  13354. /**
  13355. * this function calculates the effects of the springs in the case of unsmooth curves.
  13356. *
  13357. * @private
  13358. */
  13359. _calculateHierarchicalSpringForces: function () {
  13360. var edgeLength, edge, edgeId;
  13361. var dx, dy, fx, fy, springForce, distance;
  13362. var edges = this.edges;
  13363. // forces caused by the edges, modelled as springs
  13364. for (edgeId in edges) {
  13365. if (edges.hasOwnProperty(edgeId)) {
  13366. edge = edges[edgeId];
  13367. if (edge.connected) {
  13368. // only calculate forces if nodes are in the same sector
  13369. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  13370. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  13371. // this implies that the edges between big clusters are longer
  13372. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  13373. dx = (edge.from.x - edge.to.x);
  13374. dy = (edge.from.y - edge.to.y);
  13375. distance = Math.sqrt(dx * dx + dy * dy);
  13376. if (distance == 0) {
  13377. distance = 0.01;
  13378. }
  13379. distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance));
  13380. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  13381. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  13382. fx = dx * springForce;
  13383. fy = dy * springForce;
  13384. edge.to.fx -= fx;
  13385. edge.to.fy -= fy;
  13386. edge.from.fx += fx;
  13387. edge.from.fy += fy;
  13388. var factor = 5;
  13389. if (distance > edgeLength) {
  13390. factor = 25;
  13391. }
  13392. if (edge.from.level > edge.to.level) {
  13393. edge.to.fx -= factor*fx;
  13394. edge.to.fy -= factor*fy;
  13395. }
  13396. else if (edge.from.level < edge.to.level) {
  13397. edge.from.fx += factor*fx;
  13398. edge.from.fy += factor*fy;
  13399. }
  13400. }
  13401. }
  13402. }
  13403. }
  13404. }
  13405. };
  13406. /**
  13407. * Created by Alex on 2/10/14.
  13408. */
  13409. var barnesHutMixin = {
  13410. /**
  13411. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  13412. * The Barnes Hut method is used to speed up this N-body simulation.
  13413. *
  13414. * @private
  13415. */
  13416. _calculateNodeForces : function() {
  13417. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  13418. var node;
  13419. var nodes = this.calculationNodes;
  13420. var nodeIndices = this.calculationNodeIndices;
  13421. var nodeCount = nodeIndices.length;
  13422. this._formBarnesHutTree(nodes,nodeIndices);
  13423. var barnesHutTree = this.barnesHutTree;
  13424. // place the nodes one by one recursively
  13425. for (var i = 0; i < nodeCount; i++) {
  13426. node = nodes[nodeIndices[i]];
  13427. // starting with root is irrelevant, it never passes the BarnesHut condition
  13428. this._getForceContribution(barnesHutTree.root.children.NW,node);
  13429. this._getForceContribution(barnesHutTree.root.children.NE,node);
  13430. this._getForceContribution(barnesHutTree.root.children.SW,node);
  13431. this._getForceContribution(barnesHutTree.root.children.SE,node);
  13432. }
  13433. }
  13434. },
  13435. /**
  13436. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  13437. * If a region contains a single node, we check if it is not itself, then we apply the force.
  13438. *
  13439. * @param parentBranch
  13440. * @param node
  13441. * @private
  13442. */
  13443. _getForceContribution : function(parentBranch,node) {
  13444. // we get no force contribution from an empty region
  13445. if (parentBranch.childrenCount > 0) {
  13446. var dx,dy,distance;
  13447. // get the distance from the center of mass to the node.
  13448. dx = parentBranch.centerOfMass.x - node.x;
  13449. dy = parentBranch.centerOfMass.y - node.y;
  13450. distance = Math.sqrt(dx * dx + dy * dy);
  13451. // BarnesHut condition
  13452. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  13453. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  13454. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  13455. // duplicate code to reduce function calls to speed up program
  13456. if (distance == 0) {
  13457. distance = 0.1*Math.random();
  13458. dx = distance;
  13459. }
  13460. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  13461. var fx = dx * gravityForce;
  13462. var fy = dy * gravityForce;
  13463. node.fx += fx;
  13464. node.fy += fy;
  13465. }
  13466. else {
  13467. // Did not pass the condition, go into children if available
  13468. if (parentBranch.childrenCount == 4) {
  13469. this._getForceContribution(parentBranch.children.NW,node);
  13470. this._getForceContribution(parentBranch.children.NE,node);
  13471. this._getForceContribution(parentBranch.children.SW,node);
  13472. this._getForceContribution(parentBranch.children.SE,node);
  13473. }
  13474. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  13475. if (parentBranch.children.data.id != node.id) { // if it is not self
  13476. // duplicate code to reduce function calls to speed up program
  13477. if (distance == 0) {
  13478. distance = 0.5*Math.random();
  13479. dx = distance;
  13480. }
  13481. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  13482. var fx = dx * gravityForce;
  13483. var fy = dy * gravityForce;
  13484. node.fx += fx;
  13485. node.fy += fy;
  13486. }
  13487. }
  13488. }
  13489. }
  13490. },
  13491. /**
  13492. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  13493. *
  13494. * @param nodes
  13495. * @param nodeIndices
  13496. * @private
  13497. */
  13498. _formBarnesHutTree : function(nodes,nodeIndices) {
  13499. var node;
  13500. var nodeCount = nodeIndices.length;
  13501. var minX = Number.MAX_VALUE,
  13502. minY = Number.MAX_VALUE,
  13503. maxX =-Number.MAX_VALUE,
  13504. maxY =-Number.MAX_VALUE;
  13505. // get the range of the nodes
  13506. for (var i = 0; i < nodeCount; i++) {
  13507. var x = nodes[nodeIndices[i]].x;
  13508. var y = nodes[nodeIndices[i]].y;
  13509. if (x < minX) { minX = x; }
  13510. if (x > maxX) { maxX = x; }
  13511. if (y < minY) { minY = y; }
  13512. if (y > maxY) { maxY = y; }
  13513. }
  13514. // make the range a square
  13515. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  13516. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  13517. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  13518. var minimumTreeSize = 1e-5;
  13519. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  13520. var halfRootSize = 0.5 * rootSize;
  13521. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  13522. // construct the barnesHutTree
  13523. var barnesHutTree = {root:{
  13524. centerOfMass:{x:0,y:0}, // Center of Mass
  13525. mass:0,
  13526. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  13527. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  13528. size: rootSize,
  13529. calcSize: 1 / rootSize,
  13530. children: {data:null},
  13531. maxWidth: 0,
  13532. level: 0,
  13533. childrenCount: 4
  13534. }};
  13535. this._splitBranch(barnesHutTree.root);
  13536. // place the nodes one by one recursively
  13537. for (i = 0; i < nodeCount; i++) {
  13538. node = nodes[nodeIndices[i]];
  13539. this._placeInTree(barnesHutTree.root,node);
  13540. }
  13541. // make global
  13542. this.barnesHutTree = barnesHutTree
  13543. },
  13544. /**
  13545. * this updates the mass of a branch. this is increased by adding a node.
  13546. *
  13547. * @param parentBranch
  13548. * @param node
  13549. * @private
  13550. */
  13551. _updateBranchMass : function(parentBranch, node) {
  13552. var totalMass = parentBranch.mass + node.mass;
  13553. var totalMassInv = 1/totalMass;
  13554. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  13555. parentBranch.centerOfMass.x *= totalMassInv;
  13556. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  13557. parentBranch.centerOfMass.y *= totalMassInv;
  13558. parentBranch.mass = totalMass;
  13559. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  13560. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  13561. },
  13562. /**
  13563. * determine in which branch the node will be placed.
  13564. *
  13565. * @param parentBranch
  13566. * @param node
  13567. * @param skipMassUpdate
  13568. * @private
  13569. */
  13570. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  13571. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  13572. // update the mass of the branch.
  13573. this._updateBranchMass(parentBranch,node);
  13574. }
  13575. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  13576. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  13577. this._placeInRegion(parentBranch,node,"NW");
  13578. }
  13579. else { // in SW
  13580. this._placeInRegion(parentBranch,node,"SW");
  13581. }
  13582. }
  13583. else { // in NE or SE
  13584. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  13585. this._placeInRegion(parentBranch,node,"NE");
  13586. }
  13587. else { // in SE
  13588. this._placeInRegion(parentBranch,node,"SE");
  13589. }
  13590. }
  13591. },
  13592. /**
  13593. * actually place the node in a region (or branch)
  13594. *
  13595. * @param parentBranch
  13596. * @param node
  13597. * @param region
  13598. * @private
  13599. */
  13600. _placeInRegion : function(parentBranch,node,region) {
  13601. switch (parentBranch.children[region].childrenCount) {
  13602. case 0: // place node here
  13603. parentBranch.children[region].children.data = node;
  13604. parentBranch.children[region].childrenCount = 1;
  13605. this._updateBranchMass(parentBranch.children[region],node);
  13606. break;
  13607. case 1: // convert into children
  13608. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  13609. // we move one node a pixel and we do not put it in the tree.
  13610. if (parentBranch.children[region].children.data.x == node.x &&
  13611. parentBranch.children[region].children.data.y == node.y) {
  13612. node.x += Math.random();
  13613. node.y += Math.random();
  13614. }
  13615. else {
  13616. this._splitBranch(parentBranch.children[region]);
  13617. this._placeInTree(parentBranch.children[region],node);
  13618. }
  13619. break;
  13620. case 4: // place in branch
  13621. this._placeInTree(parentBranch.children[region],node);
  13622. break;
  13623. }
  13624. },
  13625. /**
  13626. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  13627. * after the split is complete.
  13628. *
  13629. * @param parentBranch
  13630. * @private
  13631. */
  13632. _splitBranch : function(parentBranch) {
  13633. // if the branch is shaded with a node, replace the node in the new subset.
  13634. var containedNode = null;
  13635. if (parentBranch.childrenCount == 1) {
  13636. containedNode = parentBranch.children.data;
  13637. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  13638. }
  13639. parentBranch.childrenCount = 4;
  13640. parentBranch.children.data = null;
  13641. this._insertRegion(parentBranch,"NW");
  13642. this._insertRegion(parentBranch,"NE");
  13643. this._insertRegion(parentBranch,"SW");
  13644. this._insertRegion(parentBranch,"SE");
  13645. if (containedNode != null) {
  13646. this._placeInTree(parentBranch,containedNode);
  13647. }
  13648. },
  13649. /**
  13650. * This function subdivides the region into four new segments.
  13651. * Specifically, this inserts a single new segment.
  13652. * It fills the children section of the parentBranch
  13653. *
  13654. * @param parentBranch
  13655. * @param region
  13656. * @param parentRange
  13657. * @private
  13658. */
  13659. _insertRegion : function(parentBranch, region) {
  13660. var minX,maxX,minY,maxY;
  13661. var childSize = 0.5 * parentBranch.size;
  13662. switch (region) {
  13663. case "NW":
  13664. minX = parentBranch.range.minX;
  13665. maxX = parentBranch.range.minX + childSize;
  13666. minY = parentBranch.range.minY;
  13667. maxY = parentBranch.range.minY + childSize;
  13668. break;
  13669. case "NE":
  13670. minX = parentBranch.range.minX + childSize;
  13671. maxX = parentBranch.range.maxX;
  13672. minY = parentBranch.range.minY;
  13673. maxY = parentBranch.range.minY + childSize;
  13674. break;
  13675. case "SW":
  13676. minX = parentBranch.range.minX;
  13677. maxX = parentBranch.range.minX + childSize;
  13678. minY = parentBranch.range.minY + childSize;
  13679. maxY = parentBranch.range.maxY;
  13680. break;
  13681. case "SE":
  13682. minX = parentBranch.range.minX + childSize;
  13683. maxX = parentBranch.range.maxX;
  13684. minY = parentBranch.range.minY + childSize;
  13685. maxY = parentBranch.range.maxY;
  13686. break;
  13687. }
  13688. parentBranch.children[region] = {
  13689. centerOfMass:{x:0,y:0},
  13690. mass:0,
  13691. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  13692. size: 0.5 * parentBranch.size,
  13693. calcSize: 2 * parentBranch.calcSize,
  13694. children: {data:null},
  13695. maxWidth: 0,
  13696. level: parentBranch.level+1,
  13697. childrenCount: 0
  13698. };
  13699. },
  13700. /**
  13701. * This function is for debugging purposed, it draws the tree.
  13702. *
  13703. * @param ctx
  13704. * @param color
  13705. * @private
  13706. */
  13707. _drawTree : function(ctx,color) {
  13708. if (this.barnesHutTree !== undefined) {
  13709. ctx.lineWidth = 1;
  13710. this._drawBranch(this.barnesHutTree.root,ctx,color);
  13711. }
  13712. },
  13713. /**
  13714. * This function is for debugging purposes. It draws the branches recursively.
  13715. *
  13716. * @param branch
  13717. * @param ctx
  13718. * @param color
  13719. * @private
  13720. */
  13721. _drawBranch : function(branch,ctx,color) {
  13722. if (color === undefined) {
  13723. color = "#FF0000";
  13724. }
  13725. if (branch.childrenCount == 4) {
  13726. this._drawBranch(branch.children.NW,ctx);
  13727. this._drawBranch(branch.children.NE,ctx);
  13728. this._drawBranch(branch.children.SE,ctx);
  13729. this._drawBranch(branch.children.SW,ctx);
  13730. }
  13731. ctx.strokeStyle = color;
  13732. ctx.beginPath();
  13733. ctx.moveTo(branch.range.minX,branch.range.minY);
  13734. ctx.lineTo(branch.range.maxX,branch.range.minY);
  13735. ctx.stroke();
  13736. ctx.beginPath();
  13737. ctx.moveTo(branch.range.maxX,branch.range.minY);
  13738. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  13739. ctx.stroke();
  13740. ctx.beginPath();
  13741. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  13742. ctx.lineTo(branch.range.minX,branch.range.maxY);
  13743. ctx.stroke();
  13744. ctx.beginPath();
  13745. ctx.moveTo(branch.range.minX,branch.range.maxY);
  13746. ctx.lineTo(branch.range.minX,branch.range.minY);
  13747. ctx.stroke();
  13748. /*
  13749. if (branch.mass > 0) {
  13750. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  13751. ctx.stroke();
  13752. }
  13753. */
  13754. }
  13755. };
  13756. /**
  13757. * Created by Alex on 2/10/14.
  13758. */
  13759. var repulsionMixin = {
  13760. /**
  13761. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  13762. * This field is linearly approximated.
  13763. *
  13764. * @private
  13765. */
  13766. _calculateNodeForces: function () {
  13767. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  13768. repulsingForce, node1, node2, i, j;
  13769. var nodes = this.calculationNodes;
  13770. var nodeIndices = this.calculationNodeIndices;
  13771. // approximation constants
  13772. var a_base = -2 / 3;
  13773. var b = 4 / 3;
  13774. // repulsing forces between nodes
  13775. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  13776. var minimumDistance = nodeDistance;
  13777. // we loop from i over all but the last entree in the array
  13778. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  13779. for (i = 0; i < nodeIndices.length - 1; i++) {
  13780. node1 = nodes[nodeIndices[i]];
  13781. for (j = i + 1; j < nodeIndices.length; j++) {
  13782. node2 = nodes[nodeIndices[j]];
  13783. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  13784. dx = node2.x - node1.x;
  13785. dy = node2.y - node1.y;
  13786. distance = Math.sqrt(dx * dx + dy * dy);
  13787. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  13788. var a = a_base / minimumDistance;
  13789. if (distance < 2 * minimumDistance) {
  13790. if (distance < 0.5 * minimumDistance) {
  13791. repulsingForce = 1.0;
  13792. }
  13793. else {
  13794. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  13795. }
  13796. // amplify the repulsion for clusters.
  13797. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  13798. repulsingForce = repulsingForce / distance;
  13799. fx = dx * repulsingForce;
  13800. fy = dy * repulsingForce;
  13801. node1.fx -= fx;
  13802. node1.fy -= fy;
  13803. node2.fx += fx;
  13804. node2.fy += fy;
  13805. }
  13806. }
  13807. }
  13808. }
  13809. };
  13810. var HierarchicalLayoutMixin = {
  13811. _resetLevels : function() {
  13812. for (var nodeId in this.nodes) {
  13813. if (this.nodes.hasOwnProperty(nodeId)) {
  13814. var node = this.nodes[nodeId];
  13815. if (node.preassignedLevel == false) {
  13816. node.level = -1;
  13817. }
  13818. }
  13819. }
  13820. },
  13821. /**
  13822. * This is the main function to layout the nodes in a hierarchical way.
  13823. * It checks if the node details are supplied correctly
  13824. *
  13825. * @private
  13826. */
  13827. _setupHierarchicalLayout : function() {
  13828. if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
  13829. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  13830. this.constants.hierarchicalLayout.levelSeparation *= -1;
  13831. }
  13832. else {
  13833. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  13834. }
  13835. // get the size of the largest hubs and check if the user has defined a level for a node.
  13836. var hubsize = 0;
  13837. var node, nodeId;
  13838. var definedLevel = false;
  13839. var undefinedLevel = false;
  13840. for (nodeId in this.nodes) {
  13841. if (this.nodes.hasOwnProperty(nodeId)) {
  13842. node = this.nodes[nodeId];
  13843. if (node.level != -1) {
  13844. definedLevel = true;
  13845. }
  13846. else {
  13847. undefinedLevel = true;
  13848. }
  13849. if (hubsize < node.edges.length) {
  13850. hubsize = node.edges.length;
  13851. }
  13852. }
  13853. }
  13854. // if the user defined some levels but not all, alert and run without hierarchical layout
  13855. if (undefinedLevel == true && definedLevel == true) {
  13856. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  13857. this.zoomExtent(true,this.constants.clustering.enabled);
  13858. if (!this.constants.clustering.enabled) {
  13859. this.start();
  13860. }
  13861. }
  13862. else {
  13863. // setup the system to use hierarchical method.
  13864. this._changeConstants();
  13865. // define levels if undefined by the users. Based on hubsize
  13866. if (undefinedLevel == true) {
  13867. this._determineLevels(hubsize);
  13868. }
  13869. // check the distribution of the nodes per level.
  13870. var distribution = this._getDistribution();
  13871. // place the nodes on the canvas. This also stablilizes the system.
  13872. this._placeNodesByHierarchy(distribution);
  13873. // start the simulation.
  13874. this.start();
  13875. }
  13876. }
  13877. },
  13878. /**
  13879. * This function places the nodes on the canvas based on the hierarchial distribution.
  13880. *
  13881. * @param {Object} distribution | obtained by the function this._getDistribution()
  13882. * @private
  13883. */
  13884. _placeNodesByHierarchy : function(distribution) {
  13885. var nodeId, node;
  13886. // start placing all the level 0 nodes first. Then recursively position their branches.
  13887. for (nodeId in distribution[0].nodes) {
  13888. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  13889. node = distribution[0].nodes[nodeId];
  13890. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  13891. if (node.xFixed) {
  13892. node.x = distribution[0].minPos;
  13893. node.xFixed = false;
  13894. distribution[0].minPos += distribution[0].nodeSpacing;
  13895. }
  13896. }
  13897. else {
  13898. if (node.yFixed) {
  13899. node.y = distribution[0].minPos;
  13900. node.yFixed = false;
  13901. distribution[0].minPos += distribution[0].nodeSpacing;
  13902. }
  13903. }
  13904. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  13905. }
  13906. }
  13907. // stabilize the system after positioning. This function calls zoomExtent.
  13908. this._stabilize();
  13909. },
  13910. /**
  13911. * This function get the distribution of levels based on hubsize
  13912. *
  13913. * @returns {Object}
  13914. * @private
  13915. */
  13916. _getDistribution : function() {
  13917. var distribution = {};
  13918. var nodeId, node, level;
  13919. // 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.
  13920. // the fix of X is removed after the x value has been set.
  13921. for (nodeId in this.nodes) {
  13922. if (this.nodes.hasOwnProperty(nodeId)) {
  13923. node = this.nodes[nodeId];
  13924. node.xFixed = true;
  13925. node.yFixed = true;
  13926. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  13927. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  13928. }
  13929. else {
  13930. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  13931. }
  13932. if (!distribution.hasOwnProperty(node.level)) {
  13933. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  13934. }
  13935. distribution[node.level].amount += 1;
  13936. distribution[node.level].nodes[node.id] = node;
  13937. }
  13938. }
  13939. // determine the largest amount of nodes of all levels
  13940. var maxCount = 0;
  13941. for (level in distribution) {
  13942. if (distribution.hasOwnProperty(level)) {
  13943. if (maxCount < distribution[level].amount) {
  13944. maxCount = distribution[level].amount;
  13945. }
  13946. }
  13947. }
  13948. // set the initial position and spacing of each nodes accordingly
  13949. for (level in distribution) {
  13950. if (distribution.hasOwnProperty(level)) {
  13951. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  13952. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  13953. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  13954. }
  13955. }
  13956. return distribution;
  13957. },
  13958. /**
  13959. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  13960. *
  13961. * @param hubsize
  13962. * @private
  13963. */
  13964. _determineLevels : function(hubsize) {
  13965. var nodeId, node;
  13966. // determine hubs
  13967. for (nodeId in this.nodes) {
  13968. if (this.nodes.hasOwnProperty(nodeId)) {
  13969. node = this.nodes[nodeId];
  13970. if (node.edges.length == hubsize) {
  13971. node.level = 0;
  13972. }
  13973. }
  13974. }
  13975. // branch from hubs
  13976. for (nodeId in this.nodes) {
  13977. if (this.nodes.hasOwnProperty(nodeId)) {
  13978. node = this.nodes[nodeId];
  13979. if (node.level == 0) {
  13980. this._setLevel(1,node.edges,node.id);
  13981. }
  13982. }
  13983. }
  13984. },
  13985. /**
  13986. * Since hierarchical layout does not support:
  13987. * - smooth curves (based on the physics),
  13988. * - clustering (based on dynamic node counts)
  13989. *
  13990. * We disable both features so there will be no problems.
  13991. *
  13992. * @private
  13993. */
  13994. _changeConstants : function() {
  13995. this.constants.clustering.enabled = false;
  13996. this.constants.physics.barnesHut.enabled = false;
  13997. this.constants.physics.hierarchicalRepulsion.enabled = true;
  13998. this._loadSelectedForceSolver();
  13999. this.constants.smoothCurves = false;
  14000. this._configureSmoothCurves();
  14001. },
  14002. /**
  14003. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  14004. * on a X position that ensures there will be no overlap.
  14005. *
  14006. * @param edges
  14007. * @param parentId
  14008. * @param distribution
  14009. * @param parentLevel
  14010. * @private
  14011. */
  14012. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  14013. for (var i = 0; i < edges.length; i++) {
  14014. var childNode = null;
  14015. if (edges[i].toId == parentId) {
  14016. childNode = edges[i].from;
  14017. }
  14018. else {
  14019. childNode = edges[i].to;
  14020. }
  14021. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  14022. var nodeMoved = false;
  14023. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  14024. if (childNode.xFixed && childNode.level > parentLevel) {
  14025. childNode.xFixed = false;
  14026. childNode.x = distribution[childNode.level].minPos;
  14027. nodeMoved = true;
  14028. }
  14029. }
  14030. else {
  14031. if (childNode.yFixed && childNode.level > parentLevel) {
  14032. childNode.yFixed = false;
  14033. childNode.y = distribution[childNode.level].minPos;
  14034. nodeMoved = true;
  14035. }
  14036. }
  14037. if (nodeMoved == true) {
  14038. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  14039. if (childNode.edges.length > 1) {
  14040. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  14041. }
  14042. }
  14043. }
  14044. },
  14045. /**
  14046. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  14047. *
  14048. * @param level
  14049. * @param edges
  14050. * @param parentId
  14051. * @private
  14052. */
  14053. _setLevel : function(level, edges, parentId) {
  14054. for (var i = 0; i < edges.length; i++) {
  14055. var childNode = null;
  14056. if (edges[i].toId == parentId) {
  14057. childNode = edges[i].from;
  14058. }
  14059. else {
  14060. childNode = edges[i].to;
  14061. }
  14062. if (childNode.level == -1 || childNode.level > level) {
  14063. childNode.level = level;
  14064. if (edges.length > 1) {
  14065. this._setLevel(level+1, childNode.edges, childNode.id);
  14066. }
  14067. }
  14068. }
  14069. },
  14070. /**
  14071. * Unfix nodes
  14072. *
  14073. * @private
  14074. */
  14075. _restoreNodes : function() {
  14076. for (nodeId in this.nodes) {
  14077. if (this.nodes.hasOwnProperty(nodeId)) {
  14078. this.nodes[nodeId].xFixed = false;
  14079. this.nodes[nodeId].yFixed = false;
  14080. }
  14081. }
  14082. }
  14083. };
  14084. /**
  14085. * Created by Alex on 2/4/14.
  14086. */
  14087. var manipulationMixin = {
  14088. /**
  14089. * clears the toolbar div element of children
  14090. *
  14091. * @private
  14092. */
  14093. _clearManipulatorBar : function() {
  14094. while (this.manipulationDiv.hasChildNodes()) {
  14095. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  14096. }
  14097. },
  14098. /**
  14099. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  14100. * these functions to their original functionality, we saved them in this.cachedFunctions.
  14101. * This function restores these functions to their original function.
  14102. *
  14103. * @private
  14104. */
  14105. _restoreOverloadedFunctions : function() {
  14106. for (var functionName in this.cachedFunctions) {
  14107. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  14108. this[functionName] = this.cachedFunctions[functionName];
  14109. }
  14110. }
  14111. },
  14112. /**
  14113. * Enable or disable edit-mode.
  14114. *
  14115. * @private
  14116. */
  14117. _toggleEditMode : function() {
  14118. this.editMode = !this.editMode;
  14119. var toolbar = document.getElementById("network-manipulationDiv");
  14120. var closeDiv = document.getElementById("network-manipulation-closeDiv");
  14121. var editModeDiv = document.getElementById("network-manipulation-editMode");
  14122. if (this.editMode == true) {
  14123. toolbar.style.display="block";
  14124. closeDiv.style.display="block";
  14125. editModeDiv.style.display="none";
  14126. closeDiv.onclick = this._toggleEditMode.bind(this);
  14127. }
  14128. else {
  14129. toolbar.style.display="none";
  14130. closeDiv.style.display="none";
  14131. editModeDiv.style.display="block";
  14132. closeDiv.onclick = null;
  14133. }
  14134. this._createManipulatorBar()
  14135. },
  14136. /**
  14137. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  14138. *
  14139. * @private
  14140. */
  14141. _createManipulatorBar : function() {
  14142. // remove bound functions
  14143. if (this.boundFunction) {
  14144. this.off('select', this.boundFunction);
  14145. }
  14146. if (this.edgeBeingEdited !== undefined) {
  14147. this.edgeBeingEdited._disableControlNodes();
  14148. this.edgeBeingEdited = undefined;
  14149. this.selectedControlNode = null;
  14150. }
  14151. // restore overloaded functions
  14152. this._restoreOverloadedFunctions();
  14153. // resume calculation
  14154. this.freezeSimulation = false;
  14155. // reset global variables
  14156. this.blockConnectingEdgeSelection = false;
  14157. this.forceAppendSelection = false;
  14158. if (this.editMode == true) {
  14159. while (this.manipulationDiv.hasChildNodes()) {
  14160. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  14161. }
  14162. // add the icons to the manipulator div
  14163. this.manipulationDiv.innerHTML = "" +
  14164. "<span class='network-manipulationUI add' id='network-manipulate-addNode'>" +
  14165. "<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  14166. "<div class='network-seperatorLine'></div>" +
  14167. "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
  14168. "<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  14169. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  14170. this.manipulationDiv.innerHTML += "" +
  14171. "<div class='network-seperatorLine'></div>" +
  14172. "<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" +
  14173. "<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  14174. }
  14175. else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
  14176. this.manipulationDiv.innerHTML += "" +
  14177. "<div class='network-seperatorLine'></div>" +
  14178. "<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" +
  14179. "<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>";
  14180. }
  14181. if (this._selectionIsEmpty() == false) {
  14182. this.manipulationDiv.innerHTML += "" +
  14183. "<div class='network-seperatorLine'></div>" +
  14184. "<span class='network-manipulationUI delete' id='network-manipulate-delete'>" +
  14185. "<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  14186. }
  14187. // bind the icons
  14188. var addNodeButton = document.getElementById("network-manipulate-addNode");
  14189. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  14190. var addEdgeButton = document.getElementById("network-manipulate-connectNode");
  14191. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  14192. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  14193. var editButton = document.getElementById("network-manipulate-editNode");
  14194. editButton.onclick = this._editNode.bind(this);
  14195. }
  14196. else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
  14197. var editButton = document.getElementById("network-manipulate-editEdge");
  14198. editButton.onclick = this._createEditEdgeToolbar.bind(this);
  14199. }
  14200. if (this._selectionIsEmpty() == false) {
  14201. var deleteButton = document.getElementById("network-manipulate-delete");
  14202. deleteButton.onclick = this._deleteSelected.bind(this);
  14203. }
  14204. var closeDiv = document.getElementById("network-manipulation-closeDiv");
  14205. closeDiv.onclick = this._toggleEditMode.bind(this);
  14206. this.boundFunction = this._createManipulatorBar.bind(this);
  14207. this.on('select', this.boundFunction);
  14208. }
  14209. else {
  14210. this.editModeDiv.innerHTML = "" +
  14211. "<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" +
  14212. "<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  14213. var editModeButton = document.getElementById("network-manipulate-editModeButton");
  14214. editModeButton.onclick = this._toggleEditMode.bind(this);
  14215. }
  14216. },
  14217. /**
  14218. * Create the toolbar for adding Nodes
  14219. *
  14220. * @private
  14221. */
  14222. _createAddNodeToolbar : function() {
  14223. // clear the toolbar
  14224. this._clearManipulatorBar();
  14225. if (this.boundFunction) {
  14226. this.off('select', this.boundFunction);
  14227. }
  14228. // create the toolbar contents
  14229. this.manipulationDiv.innerHTML = "" +
  14230. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  14231. "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  14232. "<div class='network-seperatorLine'></div>" +
  14233. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  14234. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  14235. // bind the icon
  14236. var backButton = document.getElementById("network-manipulate-back");
  14237. backButton.onclick = this._createManipulatorBar.bind(this);
  14238. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  14239. this.boundFunction = this._addNode.bind(this);
  14240. this.on('select', this.boundFunction);
  14241. },
  14242. /**
  14243. * create the toolbar to connect nodes
  14244. *
  14245. * @private
  14246. */
  14247. _createAddEdgeToolbar : function() {
  14248. // clear the toolbar
  14249. this._clearManipulatorBar();
  14250. this._unselectAll(true);
  14251. this.freezeSimulation = true;
  14252. if (this.boundFunction) {
  14253. this.off('select', this.boundFunction);
  14254. }
  14255. this._unselectAll();
  14256. this.forceAppendSelection = false;
  14257. this.blockConnectingEdgeSelection = true;
  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['linkDescription'] + "</span></span>";
  14264. // bind the icon
  14265. var backButton = document.getElementById("network-manipulate-back");
  14266. backButton.onclick = this._createManipulatorBar.bind(this);
  14267. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  14268. this.boundFunction = this._handleConnect.bind(this);
  14269. this.on('select', this.boundFunction);
  14270. // temporarily overload functions
  14271. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  14272. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  14273. this._handleTouch = this._handleConnect;
  14274. this._handleOnRelease = this._finishConnect;
  14275. // redraw to show the unselect
  14276. this._redraw();
  14277. },
  14278. /**
  14279. * create the toolbar to edit edges
  14280. *
  14281. * @private
  14282. */
  14283. _createEditEdgeToolbar : function() {
  14284. // clear the toolbar
  14285. this._clearManipulatorBar();
  14286. if (this.boundFunction) {
  14287. this.off('select', this.boundFunction);
  14288. }
  14289. this.edgeBeingEdited = this._getSelectedEdge();
  14290. this.edgeBeingEdited._enableControlNodes();
  14291. this.manipulationDiv.innerHTML = "" +
  14292. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  14293. "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  14294. "<div class='network-seperatorLine'></div>" +
  14295. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  14296. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>";
  14297. // bind the icon
  14298. var backButton = document.getElementById("network-manipulate-back");
  14299. backButton.onclick = this._createManipulatorBar.bind(this);
  14300. // temporarily overload functions
  14301. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  14302. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  14303. this.cachedFunctions["_handleTap"] = this._handleTap;
  14304. this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
  14305. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  14306. this._handleTouch = this._selectControlNode;
  14307. this._handleTap = function () {};
  14308. this._handleOnDrag = this._controlNodeDrag;
  14309. this._handleDragStart = function () {}
  14310. this._handleOnRelease = this._releaseControlNode;
  14311. // redraw to show the unselect
  14312. this._redraw();
  14313. },
  14314. /**
  14315. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  14316. * to walk the user through the process.
  14317. *
  14318. * @private
  14319. */
  14320. _selectControlNode : function(pointer) {
  14321. this.edgeBeingEdited.controlNodes.from.unselect();
  14322. this.edgeBeingEdited.controlNodes.to.unselect();
  14323. this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
  14324. if (this.selectedControlNode !== null) {
  14325. this.selectedControlNode.select();
  14326. this.freezeSimulation = true;
  14327. }
  14328. this._redraw();
  14329. },
  14330. /**
  14331. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  14332. * to walk the user through the process.
  14333. *
  14334. * @private
  14335. */
  14336. _controlNodeDrag : function(event) {
  14337. var pointer = this._getPointer(event.gesture.center);
  14338. if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
  14339. this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
  14340. this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
  14341. }
  14342. this._redraw();
  14343. },
  14344. _releaseControlNode : function(pointer) {
  14345. var newNode = this._getNodeAt(pointer);
  14346. if (newNode != null) {
  14347. if (this.edgeBeingEdited.controlNodes.from.selected == true) {
  14348. this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
  14349. this.edgeBeingEdited.controlNodes.from.unselect();
  14350. }
  14351. if (this.edgeBeingEdited.controlNodes.to.selected == true) {
  14352. this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
  14353. this.edgeBeingEdited.controlNodes.to.unselect();
  14354. }
  14355. }
  14356. else {
  14357. this.edgeBeingEdited._restoreControlNodes();
  14358. }
  14359. this.freezeSimulation = false;
  14360. this._redraw();
  14361. },
  14362. /**
  14363. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  14364. * to walk the user through the process.
  14365. *
  14366. * @private
  14367. */
  14368. _handleConnect : function(pointer) {
  14369. if (this._getSelectedNodeCount() == 0) {
  14370. var node = this._getNodeAt(pointer);
  14371. if (node != null) {
  14372. if (node.clusterSize > 1) {
  14373. alert("Cannot create edges to a cluster.")
  14374. }
  14375. else {
  14376. this._selectObject(node,false);
  14377. // create a node the temporary line can look at
  14378. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  14379. this.sectors['support']['nodes']['targetNode'].x = node.x;
  14380. this.sectors['support']['nodes']['targetNode'].y = node.y;
  14381. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  14382. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  14383. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  14384. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  14385. // create a temporary edge
  14386. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  14387. this.edges['connectionEdge'].from = node;
  14388. this.edges['connectionEdge'].connected = true;
  14389. this.edges['connectionEdge'].smooth = true;
  14390. this.edges['connectionEdge'].selected = true;
  14391. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  14392. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  14393. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  14394. this._handleOnDrag = function(event) {
  14395. var pointer = this._getPointer(event.gesture.center);
  14396. this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
  14397. this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  14398. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
  14399. this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  14400. };
  14401. this.moving = true;
  14402. this.start();
  14403. }
  14404. }
  14405. }
  14406. },
  14407. _finishConnect : function(pointer) {
  14408. if (this._getSelectedNodeCount() == 1) {
  14409. // restore the drag function
  14410. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  14411. delete this.cachedFunctions["_handleOnDrag"];
  14412. // remember the edge id
  14413. var connectFromId = this.edges['connectionEdge'].fromId;
  14414. // remove the temporary nodes and edge
  14415. delete this.edges['connectionEdge'];
  14416. delete this.sectors['support']['nodes']['targetNode'];
  14417. delete this.sectors['support']['nodes']['targetViaNode'];
  14418. var node = this._getNodeAt(pointer);
  14419. if (node != null) {
  14420. if (node.clusterSize > 1) {
  14421. alert("Cannot create edges to a cluster.")
  14422. }
  14423. else {
  14424. this._createEdge(connectFromId,node.id);
  14425. this._createManipulatorBar();
  14426. }
  14427. }
  14428. this._unselectAll();
  14429. }
  14430. },
  14431. /**
  14432. * Adds a node on the specified location
  14433. */
  14434. _addNode : function() {
  14435. if (this._selectionIsEmpty() && this.editMode == true) {
  14436. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  14437. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  14438. if (this.triggerFunctions.add) {
  14439. if (this.triggerFunctions.add.length == 2) {
  14440. var me = this;
  14441. this.triggerFunctions.add(defaultData, function(finalizedData) {
  14442. me.nodesData.add(finalizedData);
  14443. me._createManipulatorBar();
  14444. me.moving = true;
  14445. me.start();
  14446. });
  14447. }
  14448. else {
  14449. alert(this.constants.labels['addError']);
  14450. this._createManipulatorBar();
  14451. this.moving = true;
  14452. this.start();
  14453. }
  14454. }
  14455. else {
  14456. this.nodesData.add(defaultData);
  14457. this._createManipulatorBar();
  14458. this.moving = true;
  14459. this.start();
  14460. }
  14461. }
  14462. },
  14463. /**
  14464. * connect two nodes with a new edge.
  14465. *
  14466. * @private
  14467. */
  14468. _createEdge : function(sourceNodeId,targetNodeId) {
  14469. if (this.editMode == true) {
  14470. var defaultData = {from:sourceNodeId, to:targetNodeId};
  14471. if (this.triggerFunctions.connect) {
  14472. if (this.triggerFunctions.connect.length == 2) {
  14473. var me = this;
  14474. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  14475. me.edgesData.add(finalizedData);
  14476. me.moving = true;
  14477. me.start();
  14478. });
  14479. }
  14480. else {
  14481. alert(this.constants.labels["linkError"]);
  14482. this.moving = true;
  14483. this.start();
  14484. }
  14485. }
  14486. else {
  14487. this.edgesData.add(defaultData);
  14488. this.moving = true;
  14489. this.start();
  14490. }
  14491. }
  14492. },
  14493. /**
  14494. * connect two nodes with a new edge.
  14495. *
  14496. * @private
  14497. */
  14498. _editEdge : function(sourceNodeId,targetNodeId) {
  14499. if (this.editMode == true) {
  14500. var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
  14501. if (this.triggerFunctions.editEdge) {
  14502. if (this.triggerFunctions.editEdge.length == 2) {
  14503. var me = this;
  14504. this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
  14505. me.edgesData.update(finalizedData);
  14506. me.moving = true;
  14507. me.start();
  14508. });
  14509. }
  14510. else {
  14511. alert(this.constants.labels["linkError"]);
  14512. this.moving = true;
  14513. this.start();
  14514. }
  14515. }
  14516. else {
  14517. this.edgesData.update(defaultData);
  14518. this.moving = true;
  14519. this.start();
  14520. }
  14521. }
  14522. },
  14523. /**
  14524. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  14525. *
  14526. * @private
  14527. */
  14528. _editNode : function() {
  14529. if (this.triggerFunctions.edit && this.editMode == true) {
  14530. var node = this._getSelectedNode();
  14531. var data = {id:node.id,
  14532. label: node.label,
  14533. group: node.group,
  14534. shape: node.shape,
  14535. color: {
  14536. background:node.color.background,
  14537. border:node.color.border,
  14538. highlight: {
  14539. background:node.color.highlight.background,
  14540. border:node.color.highlight.border
  14541. }
  14542. }};
  14543. if (this.triggerFunctions.edit.length == 2) {
  14544. var me = this;
  14545. this.triggerFunctions.edit(data, function (finalizedData) {
  14546. me.nodesData.update(finalizedData);
  14547. me._createManipulatorBar();
  14548. me.moving = true;
  14549. me.start();
  14550. });
  14551. }
  14552. else {
  14553. alert(this.constants.labels["editError"]);
  14554. }
  14555. }
  14556. else {
  14557. alert(this.constants.labels["editBoundError"]);
  14558. }
  14559. },
  14560. /**
  14561. * delete everything in the selection
  14562. *
  14563. * @private
  14564. */
  14565. _deleteSelected : function() {
  14566. if (!this._selectionIsEmpty() && this.editMode == true) {
  14567. if (!this._clusterInSelection()) {
  14568. var selectedNodes = this.getSelectedNodes();
  14569. var selectedEdges = this.getSelectedEdges();
  14570. if (this.triggerFunctions.del) {
  14571. var me = this;
  14572. var data = {nodes: selectedNodes, edges: selectedEdges};
  14573. if (this.triggerFunctions.del.length = 2) {
  14574. this.triggerFunctions.del(data, function (finalizedData) {
  14575. me.edgesData.remove(finalizedData.edges);
  14576. me.nodesData.remove(finalizedData.nodes);
  14577. me._unselectAll();
  14578. me.moving = true;
  14579. me.start();
  14580. });
  14581. }
  14582. else {
  14583. alert(this.constants.labels["deleteError"])
  14584. }
  14585. }
  14586. else {
  14587. this.edgesData.remove(selectedEdges);
  14588. this.nodesData.remove(selectedNodes);
  14589. this._unselectAll();
  14590. this.moving = true;
  14591. this.start();
  14592. }
  14593. }
  14594. else {
  14595. alert(this.constants.labels["deleteClusterError"]);
  14596. }
  14597. }
  14598. }
  14599. };
  14600. /**
  14601. * Creation of the SectorMixin var.
  14602. *
  14603. * This contains all the functions the Network object can use to employ the sector system.
  14604. * The sector system is always used by Network, though the benefits only apply to the use of clustering.
  14605. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  14606. *
  14607. * Alex de Mulder
  14608. * 21-01-2013
  14609. */
  14610. var SectorMixin = {
  14611. /**
  14612. * This function is only called by the setData function of the Network object.
  14613. * This loads the global references into the active sector. This initializes the sector.
  14614. *
  14615. * @private
  14616. */
  14617. _putDataInSector : function() {
  14618. this.sectors["active"][this._sector()].nodes = this.nodes;
  14619. this.sectors["active"][this._sector()].edges = this.edges;
  14620. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  14621. },
  14622. /**
  14623. * /**
  14624. * This function sets the global references to nodes, edges and nodeIndices back to
  14625. * those of the supplied (active) sector. If a type is defined, do the specific type
  14626. *
  14627. * @param {String} sectorId
  14628. * @param {String} [sectorType] | "active" or "frozen"
  14629. * @private
  14630. */
  14631. _switchToSector : function(sectorId, sectorType) {
  14632. if (sectorType === undefined || sectorType == "active") {
  14633. this._switchToActiveSector(sectorId);
  14634. }
  14635. else {
  14636. this._switchToFrozenSector(sectorId);
  14637. }
  14638. },
  14639. /**
  14640. * This function sets the global references to nodes, edges and nodeIndices back to
  14641. * those of the supplied active sector.
  14642. *
  14643. * @param sectorId
  14644. * @private
  14645. */
  14646. _switchToActiveSector : function(sectorId) {
  14647. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  14648. this.nodes = this.sectors["active"][sectorId]["nodes"];
  14649. this.edges = this.sectors["active"][sectorId]["edges"];
  14650. },
  14651. /**
  14652. * This function sets the global references to nodes, edges and nodeIndices back to
  14653. * those of the supplied active sector.
  14654. *
  14655. * @param sectorId
  14656. * @private
  14657. */
  14658. _switchToSupportSector : function() {
  14659. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  14660. this.nodes = this.sectors["support"]["nodes"];
  14661. this.edges = this.sectors["support"]["edges"];
  14662. },
  14663. /**
  14664. * This function sets the global references to nodes, edges and nodeIndices back to
  14665. * those of the supplied frozen sector.
  14666. *
  14667. * @param sectorId
  14668. * @private
  14669. */
  14670. _switchToFrozenSector : function(sectorId) {
  14671. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  14672. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  14673. this.edges = this.sectors["frozen"][sectorId]["edges"];
  14674. },
  14675. /**
  14676. * This function sets the global references to nodes, edges and nodeIndices back to
  14677. * those of the currently active sector.
  14678. *
  14679. * @private
  14680. */
  14681. _loadLatestSector : function() {
  14682. this._switchToSector(this._sector());
  14683. },
  14684. /**
  14685. * This function returns the currently active sector Id
  14686. *
  14687. * @returns {String}
  14688. * @private
  14689. */
  14690. _sector : function() {
  14691. return this.activeSector[this.activeSector.length-1];
  14692. },
  14693. /**
  14694. * This function returns the previously active sector Id
  14695. *
  14696. * @returns {String}
  14697. * @private
  14698. */
  14699. _previousSector : function() {
  14700. if (this.activeSector.length > 1) {
  14701. return this.activeSector[this.activeSector.length-2];
  14702. }
  14703. else {
  14704. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  14705. }
  14706. },
  14707. /**
  14708. * We add the active sector at the end of the this.activeSector array
  14709. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  14710. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  14711. *
  14712. * @param newId
  14713. * @private
  14714. */
  14715. _setActiveSector : function(newId) {
  14716. this.activeSector.push(newId);
  14717. },
  14718. /**
  14719. * We remove the currently active sector id from the active sector stack. This happens when
  14720. * we reactivate the previously active sector
  14721. *
  14722. * @private
  14723. */
  14724. _forgetLastSector : function() {
  14725. this.activeSector.pop();
  14726. },
  14727. /**
  14728. * This function creates a new active sector with the supplied newId. This newId
  14729. * is the expanding node id.
  14730. *
  14731. * @param {String} newId | Id of the new active sector
  14732. * @private
  14733. */
  14734. _createNewSector : function(newId) {
  14735. // create the new sector
  14736. this.sectors["active"][newId] = {"nodes":{},
  14737. "edges":{},
  14738. "nodeIndices":[],
  14739. "formationScale": this.scale,
  14740. "drawingNode": undefined};
  14741. // create the new sector render node. This gives visual feedback that you are in a new sector.
  14742. this.sectors["active"][newId]['drawingNode'] = new Node(
  14743. {id:newId,
  14744. color: {
  14745. background: "#eaefef",
  14746. border: "495c5e"
  14747. }
  14748. },{},{},this.constants);
  14749. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  14750. },
  14751. /**
  14752. * This function removes the currently active sector. This is called when we create a new
  14753. * active sector.
  14754. *
  14755. * @param {String} sectorId | Id of the active sector that will be removed
  14756. * @private
  14757. */
  14758. _deleteActiveSector : function(sectorId) {
  14759. delete this.sectors["active"][sectorId];
  14760. },
  14761. /**
  14762. * This function removes the currently active sector. This is called when we reactivate
  14763. * the previously active sector.
  14764. *
  14765. * @param {String} sectorId | Id of the active sector that will be removed
  14766. * @private
  14767. */
  14768. _deleteFrozenSector : function(sectorId) {
  14769. delete this.sectors["frozen"][sectorId];
  14770. },
  14771. /**
  14772. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  14773. * We copy the references, then delete the active entree.
  14774. *
  14775. * @param sectorId
  14776. * @private
  14777. */
  14778. _freezeSector : function(sectorId) {
  14779. // we move the set references from the active to the frozen stack.
  14780. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  14781. // we have moved the sector data into the frozen set, we now remove it from the active set
  14782. this._deleteActiveSector(sectorId);
  14783. },
  14784. /**
  14785. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  14786. * object to the "active" object.
  14787. *
  14788. * @param sectorId
  14789. * @private
  14790. */
  14791. _activateSector : function(sectorId) {
  14792. // we move the set references from the frozen to the active stack.
  14793. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  14794. // we have moved the sector data into the active set, we now remove it from the frozen stack
  14795. this._deleteFrozenSector(sectorId);
  14796. },
  14797. /**
  14798. * This function merges the data from the currently active sector with a frozen sector. This is used
  14799. * in the process of reverting back to the previously active sector.
  14800. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  14801. * upon the creation of a new active sector.
  14802. *
  14803. * @param sectorId
  14804. * @private
  14805. */
  14806. _mergeThisWithFrozen : function(sectorId) {
  14807. // copy all nodes
  14808. for (var nodeId in this.nodes) {
  14809. if (this.nodes.hasOwnProperty(nodeId)) {
  14810. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  14811. }
  14812. }
  14813. // copy all edges (if not fully clustered, else there are no edges)
  14814. for (var edgeId in this.edges) {
  14815. if (this.edges.hasOwnProperty(edgeId)) {
  14816. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  14817. }
  14818. }
  14819. // merge the nodeIndices
  14820. for (var i = 0; i < this.nodeIndices.length; i++) {
  14821. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  14822. }
  14823. },
  14824. /**
  14825. * This clusters the sector to one cluster. It was a single cluster before this process started so
  14826. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  14827. *
  14828. * @private
  14829. */
  14830. _collapseThisToSingleCluster : function() {
  14831. this.clusterToFit(1,false);
  14832. },
  14833. /**
  14834. * We create a new active sector from the node that we want to open.
  14835. *
  14836. * @param node
  14837. * @private
  14838. */
  14839. _addSector : function(node) {
  14840. // this is the currently active sector
  14841. var sector = this._sector();
  14842. // // this should allow me to select nodes from a frozen set.
  14843. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  14844. // console.log("the node is part of the active sector");
  14845. // }
  14846. // else {
  14847. // console.log("I dont know what happened!!");
  14848. // }
  14849. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  14850. delete this.nodes[node.id];
  14851. var unqiueIdentifier = util.randomUUID();
  14852. // we fully freeze the currently active sector
  14853. this._freezeSector(sector);
  14854. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  14855. this._createNewSector(unqiueIdentifier);
  14856. // we add the active sector to the sectors array to be able to revert these steps later on
  14857. this._setActiveSector(unqiueIdentifier);
  14858. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  14859. this._switchToSector(this._sector());
  14860. // finally we add the node we removed from our previous active sector to the new active sector
  14861. this.nodes[node.id] = node;
  14862. },
  14863. /**
  14864. * We close the sector that is currently open and revert back to the one before.
  14865. * If the active sector is the "default" sector, nothing happens.
  14866. *
  14867. * @private
  14868. */
  14869. _collapseSector : function() {
  14870. // the currently active sector
  14871. var sector = this._sector();
  14872. // we cannot collapse the default sector
  14873. if (sector != "default") {
  14874. if ((this.nodeIndices.length == 1) ||
  14875. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  14876. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  14877. var previousSector = this._previousSector();
  14878. // we collapse the sector back to a single cluster
  14879. this._collapseThisToSingleCluster();
  14880. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  14881. // This previous sector is the one we will reactivate
  14882. this._mergeThisWithFrozen(previousSector);
  14883. // the previously active (frozen) sector now has all the data from the currently active sector.
  14884. // we can now delete the active sector.
  14885. this._deleteActiveSector(sector);
  14886. // we activate the previously active (and currently frozen) sector.
  14887. this._activateSector(previousSector);
  14888. // we load the references from the newly active sector into the global references
  14889. this._switchToSector(previousSector);
  14890. // we forget the previously active sector because we reverted to the one before
  14891. this._forgetLastSector();
  14892. // finally, we update the node index list.
  14893. this._updateNodeIndexList();
  14894. // we refresh the list with calulation nodes and calculation node indices.
  14895. this._updateCalculationNodes();
  14896. }
  14897. }
  14898. },
  14899. /**
  14900. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  14901. *
  14902. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14903. * | we dont pass the function itself because then the "this" is the window object
  14904. * | instead of the Network object
  14905. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14906. * @private
  14907. */
  14908. _doInAllActiveSectors : function(runFunction,argument) {
  14909. if (argument === undefined) {
  14910. for (var sector in this.sectors["active"]) {
  14911. if (this.sectors["active"].hasOwnProperty(sector)) {
  14912. // switch the global references to those of this sector
  14913. this._switchToActiveSector(sector);
  14914. this[runFunction]();
  14915. }
  14916. }
  14917. }
  14918. else {
  14919. for (var sector in this.sectors["active"]) {
  14920. if (this.sectors["active"].hasOwnProperty(sector)) {
  14921. // switch the global references to those of this sector
  14922. this._switchToActiveSector(sector);
  14923. var args = Array.prototype.splice.call(arguments, 1);
  14924. if (args.length > 1) {
  14925. this[runFunction](args[0],args[1]);
  14926. }
  14927. else {
  14928. this[runFunction](argument);
  14929. }
  14930. }
  14931. }
  14932. }
  14933. // we revert the global references back to our active sector
  14934. this._loadLatestSector();
  14935. },
  14936. /**
  14937. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  14938. *
  14939. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14940. * | we dont pass the function itself because then the "this" is the window object
  14941. * | instead of the Network object
  14942. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14943. * @private
  14944. */
  14945. _doInSupportSector : function(runFunction,argument) {
  14946. if (argument === undefined) {
  14947. this._switchToSupportSector();
  14948. this[runFunction]();
  14949. }
  14950. else {
  14951. this._switchToSupportSector();
  14952. var args = Array.prototype.splice.call(arguments, 1);
  14953. if (args.length > 1) {
  14954. this[runFunction](args[0],args[1]);
  14955. }
  14956. else {
  14957. this[runFunction](argument);
  14958. }
  14959. }
  14960. // we revert the global references back to our active sector
  14961. this._loadLatestSector();
  14962. },
  14963. /**
  14964. * This runs a function in all frozen sectors. This is used in the _redraw().
  14965. *
  14966. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  14967. * | we don't pass the function itself because then the "this" is the window object
  14968. * | instead of the Network object
  14969. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  14970. * @private
  14971. */
  14972. _doInAllFrozenSectors : function(runFunction,argument) {
  14973. if (argument === undefined) {
  14974. for (var sector in this.sectors["frozen"]) {
  14975. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  14976. // switch the global references to those of this sector
  14977. this._switchToFrozenSector(sector);
  14978. this[runFunction]();
  14979. }
  14980. }
  14981. }
  14982. else {
  14983. for (var sector in this.sectors["frozen"]) {
  14984. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  14985. // switch the global references to those of this sector
  14986. this._switchToFrozenSector(sector);
  14987. var args = Array.prototype.splice.call(arguments, 1);
  14988. if (args.length > 1) {
  14989. this[runFunction](args[0],args[1]);
  14990. }
  14991. else {
  14992. this[runFunction](argument);
  14993. }
  14994. }
  14995. }
  14996. }
  14997. this._loadLatestSector();
  14998. },
  14999. /**
  15000. * This runs a function in all sectors. This is used in the _redraw().
  15001. *
  15002. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  15003. * | we don't pass the function itself because then the "this" is the window object
  15004. * | instead of the Network object
  15005. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  15006. * @private
  15007. */
  15008. _doInAllSectors : function(runFunction,argument) {
  15009. var args = Array.prototype.splice.call(arguments, 1);
  15010. if (argument === undefined) {
  15011. this._doInAllActiveSectors(runFunction);
  15012. this._doInAllFrozenSectors(runFunction);
  15013. }
  15014. else {
  15015. if (args.length > 1) {
  15016. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  15017. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  15018. }
  15019. else {
  15020. this._doInAllActiveSectors(runFunction,argument);
  15021. this._doInAllFrozenSectors(runFunction,argument);
  15022. }
  15023. }
  15024. },
  15025. /**
  15026. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  15027. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  15028. *
  15029. * @private
  15030. */
  15031. _clearNodeIndexList : function() {
  15032. var sector = this._sector();
  15033. this.sectors["active"][sector]["nodeIndices"] = [];
  15034. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  15035. },
  15036. /**
  15037. * Draw the encompassing sector node
  15038. *
  15039. * @param ctx
  15040. * @param sectorType
  15041. * @private
  15042. */
  15043. _drawSectorNodes : function(ctx,sectorType) {
  15044. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  15045. for (var sector in this.sectors[sectorType]) {
  15046. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  15047. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  15048. this._switchToSector(sector,sectorType);
  15049. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  15050. for (var nodeId in this.nodes) {
  15051. if (this.nodes.hasOwnProperty(nodeId)) {
  15052. node = this.nodes[nodeId];
  15053. node.resize(ctx);
  15054. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  15055. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  15056. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  15057. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  15058. }
  15059. }
  15060. node = this.sectors[sectorType][sector]["drawingNode"];
  15061. node.x = 0.5 * (maxX + minX);
  15062. node.y = 0.5 * (maxY + minY);
  15063. node.width = 2 * (node.x - minX);
  15064. node.height = 2 * (node.y - minY);
  15065. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  15066. node.setScale(this.scale);
  15067. node._drawCircle(ctx);
  15068. }
  15069. }
  15070. }
  15071. },
  15072. _drawAllSectorNodes : function(ctx) {
  15073. this._drawSectorNodes(ctx,"frozen");
  15074. this._drawSectorNodes(ctx,"active");
  15075. this._loadLatestSector();
  15076. }
  15077. };
  15078. /**
  15079. * Creation of the ClusterMixin var.
  15080. *
  15081. * This contains all the functions the Network object can use to employ clustering
  15082. *
  15083. * Alex de Mulder
  15084. * 21-01-2013
  15085. */
  15086. var ClusterMixin = {
  15087. /**
  15088. * This is only called in the constructor of the network object
  15089. *
  15090. */
  15091. startWithClustering : function() {
  15092. // cluster if the data set is big
  15093. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  15094. // updates the lables after clustering
  15095. this.updateLabels();
  15096. // this is called here because if clusterin is disabled, the start and stabilize are called in
  15097. // the setData function.
  15098. if (this.stabilize) {
  15099. this._stabilize();
  15100. }
  15101. this.start();
  15102. },
  15103. /**
  15104. * This function clusters until the initialMaxNodes has been reached
  15105. *
  15106. * @param {Number} maxNumberOfNodes
  15107. * @param {Boolean} reposition
  15108. */
  15109. clusterToFit : function(maxNumberOfNodes, reposition) {
  15110. var numberOfNodes = this.nodeIndices.length;
  15111. var maxLevels = 50;
  15112. var level = 0;
  15113. // we first cluster the hubs, then we pull in the outliers, repeat
  15114. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  15115. if (level % 3 == 0) {
  15116. this.forceAggregateHubs(true);
  15117. this.normalizeClusterLevels();
  15118. }
  15119. else {
  15120. this.increaseClusterLevel(); // this also includes a cluster normalization
  15121. }
  15122. numberOfNodes = this.nodeIndices.length;
  15123. level += 1;
  15124. }
  15125. // after the clustering we reposition the nodes to reduce the initial chaos
  15126. if (level > 0 && reposition == true) {
  15127. this.repositionNodes();
  15128. }
  15129. this._updateCalculationNodes();
  15130. },
  15131. /**
  15132. * This function can be called to open up a specific cluster. It is only called by
  15133. * It will unpack the cluster back one level.
  15134. *
  15135. * @param node | Node object: cluster to open.
  15136. */
  15137. openCluster : function(node) {
  15138. var isMovingBeforeClustering = this.moving;
  15139. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  15140. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  15141. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  15142. this._addSector(node);
  15143. var level = 0;
  15144. // we decluster until we reach a decent number of nodes
  15145. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  15146. this.decreaseClusterLevel();
  15147. level += 1;
  15148. }
  15149. }
  15150. else {
  15151. this._expandClusterNode(node,false,true);
  15152. // update the index list, dynamic edges and labels
  15153. this._updateNodeIndexList();
  15154. this._updateDynamicEdges();
  15155. this._updateCalculationNodes();
  15156. this.updateLabels();
  15157. }
  15158. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  15159. if (this.moving != isMovingBeforeClustering) {
  15160. this.start();
  15161. }
  15162. },
  15163. /**
  15164. * This calls the updateClustes with default arguments
  15165. */
  15166. updateClustersDefault : function() {
  15167. if (this.constants.clustering.enabled == true) {
  15168. this.updateClusters(0,false,false);
  15169. }
  15170. },
  15171. /**
  15172. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  15173. * be clustered with their connected node. This can be repeated as many times as needed.
  15174. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  15175. */
  15176. increaseClusterLevel : function() {
  15177. this.updateClusters(-1,false,true);
  15178. },
  15179. /**
  15180. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  15181. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  15182. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  15183. */
  15184. decreaseClusterLevel : function() {
  15185. this.updateClusters(1,false,true);
  15186. },
  15187. /**
  15188. * This is the main clustering function. It clusters and declusters on zoom or forced
  15189. * This function clusters on zoom, it can be called with a predefined zoom direction
  15190. * If out, check if we can form clusters, if in, check if we can open clusters.
  15191. * This function is only called from _zoom()
  15192. *
  15193. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  15194. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  15195. * @param {Boolean} force | enabled or disable forcing
  15196. * @param {Boolean} doNotStart | if true do not call start
  15197. *
  15198. */
  15199. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  15200. var isMovingBeforeClustering = this.moving;
  15201. var amountOfNodes = this.nodeIndices.length;
  15202. // on zoom out collapse the sector if the scale is at the level the sector was made
  15203. if (this.previousScale > this.scale && zoomDirection == 0) {
  15204. this._collapseSector();
  15205. }
  15206. // check if we zoom in or out
  15207. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  15208. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  15209. // outer nodes determines if it is being clustered
  15210. this._formClusters(force);
  15211. }
  15212. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  15213. if (force == true) {
  15214. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  15215. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  15216. this._openClusters(recursive,force);
  15217. }
  15218. else {
  15219. // if a cluster takes up a set percentage of the active window
  15220. this._openClustersBySize();
  15221. }
  15222. }
  15223. this._updateNodeIndexList();
  15224. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  15225. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  15226. this._aggregateHubs(force);
  15227. this._updateNodeIndexList();
  15228. }
  15229. // we now reduce chains.
  15230. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  15231. this.handleChains();
  15232. this._updateNodeIndexList();
  15233. }
  15234. this.previousScale = this.scale;
  15235. // rest of the update the index list, dynamic edges and labels
  15236. this._updateDynamicEdges();
  15237. this.updateLabels();
  15238. // if a cluster was formed, we increase the clusterSession
  15239. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  15240. this.clusterSession += 1;
  15241. // if clusters have been made, we normalize the cluster level
  15242. this.normalizeClusterLevels();
  15243. }
  15244. if (doNotStart == false || doNotStart === undefined) {
  15245. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  15246. if (this.moving != isMovingBeforeClustering) {
  15247. this.start();
  15248. }
  15249. }
  15250. this._updateCalculationNodes();
  15251. },
  15252. /**
  15253. * This function handles the chains. It is called on every updateClusters().
  15254. */
  15255. handleChains : function() {
  15256. // after clustering we check how many chains there are
  15257. var chainPercentage = this._getChainFraction();
  15258. if (chainPercentage > this.constants.clustering.chainThreshold) {
  15259. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  15260. }
  15261. },
  15262. /**
  15263. * this functions starts clustering by hubs
  15264. * The minimum hub threshold is set globally
  15265. *
  15266. * @private
  15267. */
  15268. _aggregateHubs : function(force) {
  15269. this._getHubSize();
  15270. this._formClustersByHub(force,false);
  15271. },
  15272. /**
  15273. * This function is fired by keypress. It forces hubs to form.
  15274. *
  15275. */
  15276. forceAggregateHubs : function(doNotStart) {
  15277. var isMovingBeforeClustering = this.moving;
  15278. var amountOfNodes = this.nodeIndices.length;
  15279. this._aggregateHubs(true);
  15280. // update the index list, dynamic edges and labels
  15281. this._updateNodeIndexList();
  15282. this._updateDynamicEdges();
  15283. this.updateLabels();
  15284. // if a cluster was formed, we increase the clusterSession
  15285. if (this.nodeIndices.length != amountOfNodes) {
  15286. this.clusterSession += 1;
  15287. }
  15288. if (doNotStart == false || doNotStart === undefined) {
  15289. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  15290. if (this.moving != isMovingBeforeClustering) {
  15291. this.start();
  15292. }
  15293. }
  15294. },
  15295. /**
  15296. * If a cluster takes up more than a set percentage of the screen, open the cluster
  15297. *
  15298. * @private
  15299. */
  15300. _openClustersBySize : function() {
  15301. for (var nodeId in this.nodes) {
  15302. if (this.nodes.hasOwnProperty(nodeId)) {
  15303. var node = this.nodes[nodeId];
  15304. if (node.inView() == true) {
  15305. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  15306. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  15307. this.openCluster(node);
  15308. }
  15309. }
  15310. }
  15311. }
  15312. },
  15313. /**
  15314. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  15315. * has to be opened based on the current zoom level.
  15316. *
  15317. * @private
  15318. */
  15319. _openClusters : function(recursive,force) {
  15320. for (var i = 0; i < this.nodeIndices.length; i++) {
  15321. var node = this.nodes[this.nodeIndices[i]];
  15322. this._expandClusterNode(node,recursive,force);
  15323. this._updateCalculationNodes();
  15324. }
  15325. },
  15326. /**
  15327. * This function checks if a node has to be opened. This is done by checking the zoom level.
  15328. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  15329. * This recursive behaviour is optional and can be set by the recursive argument.
  15330. *
  15331. * @param {Node} parentNode | to check for cluster and expand
  15332. * @param {Boolean} recursive | enabled or disable recursive calling
  15333. * @param {Boolean} force | enabled or disable forcing
  15334. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  15335. * @private
  15336. */
  15337. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  15338. // first check if node is a cluster
  15339. if (parentNode.clusterSize > 1) {
  15340. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  15341. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  15342. openAll = true;
  15343. }
  15344. recursive = openAll ? true : recursive;
  15345. // if the last child has been added on a smaller scale than current scale decluster
  15346. if (parentNode.formationScale < this.scale || force == true) {
  15347. // we will check if any of the contained child nodes should be removed from the cluster
  15348. for (var containedNodeId in parentNode.containedNodes) {
  15349. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  15350. var childNode = parentNode.containedNodes[containedNodeId];
  15351. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  15352. // the largest cluster is the one that comes from outside
  15353. if (force == true) {
  15354. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  15355. || openAll) {
  15356. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  15357. }
  15358. }
  15359. else {
  15360. if (this._nodeInActiveArea(parentNode)) {
  15361. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  15362. }
  15363. }
  15364. }
  15365. }
  15366. }
  15367. }
  15368. },
  15369. /**
  15370. * ONLY CALLED FROM _expandClusterNode
  15371. *
  15372. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  15373. * the child node from the parent contained_node object and put it back into the global nodes object.
  15374. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  15375. *
  15376. * @param {Node} parentNode | the parent node
  15377. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  15378. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  15379. * With force and recursive both true, the entire cluster is unpacked
  15380. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  15381. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  15382. * @private
  15383. */
  15384. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  15385. var childNode = parentNode.containedNodes[containedNodeId];
  15386. // if child node has been added on smaller scale than current, kick out
  15387. if (childNode.formationScale < this.scale || force == true) {
  15388. // unselect all selected items
  15389. this._unselectAll();
  15390. // put the child node back in the global nodes object
  15391. this.nodes[containedNodeId] = childNode;
  15392. // release the contained edges from this childNode back into the global edges
  15393. this._releaseContainedEdges(parentNode,childNode);
  15394. // reconnect rerouted edges to the childNode
  15395. this._connectEdgeBackToChild(parentNode,childNode);
  15396. // validate all edges in dynamicEdges
  15397. this._validateEdges(parentNode);
  15398. // undo the changes from the clustering operation on the parent node
  15399. parentNode.mass -= childNode.mass;
  15400. parentNode.clusterSize -= childNode.clusterSize;
  15401. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  15402. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  15403. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  15404. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  15405. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  15406. // remove node from the list
  15407. delete parentNode.containedNodes[containedNodeId];
  15408. // check if there are other childs with this clusterSession in the parent.
  15409. var othersPresent = false;
  15410. for (var childNodeId in parentNode.containedNodes) {
  15411. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  15412. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  15413. othersPresent = true;
  15414. break;
  15415. }
  15416. }
  15417. }
  15418. // if there are no others, remove the cluster session from the list
  15419. if (othersPresent == false) {
  15420. parentNode.clusterSessions.pop();
  15421. }
  15422. this._repositionBezierNodes(childNode);
  15423. // this._repositionBezierNodes(parentNode);
  15424. // remove the clusterSession from the child node
  15425. childNode.clusterSession = 0;
  15426. // recalculate the size of the node on the next time the node is rendered
  15427. parentNode.clearSizeCache();
  15428. // restart the simulation to reorganise all nodes
  15429. this.moving = true;
  15430. }
  15431. // check if a further expansion step is possible if recursivity is enabled
  15432. if (recursive == true) {
  15433. this._expandClusterNode(childNode,recursive,force,openAll);
  15434. }
  15435. },
  15436. /**
  15437. * position the bezier nodes at the center of the edges
  15438. *
  15439. * @param node
  15440. * @private
  15441. */
  15442. _repositionBezierNodes : function(node) {
  15443. for (var i = 0; i < node.dynamicEdges.length; i++) {
  15444. node.dynamicEdges[i].positionBezierNode();
  15445. }
  15446. },
  15447. /**
  15448. * This function checks if any nodes at the end of their trees have edges below a threshold length
  15449. * This function is called only from updateClusters()
  15450. * forceLevelCollapse ignores the length of the edge and collapses one level
  15451. * This means that a node with only one edge will be clustered with its connected node
  15452. *
  15453. * @private
  15454. * @param {Boolean} force
  15455. */
  15456. _formClusters : function(force) {
  15457. if (force == false) {
  15458. this._formClustersByZoom();
  15459. }
  15460. else {
  15461. this._forceClustersByZoom();
  15462. }
  15463. },
  15464. /**
  15465. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  15466. *
  15467. * @private
  15468. */
  15469. _formClustersByZoom : function() {
  15470. var dx,dy,length,
  15471. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  15472. // check if any edges are shorter than minLength and start the clustering
  15473. // the clustering favours the node with the larger mass
  15474. for (var edgeId in this.edges) {
  15475. if (this.edges.hasOwnProperty(edgeId)) {
  15476. var edge = this.edges[edgeId];
  15477. if (edge.connected) {
  15478. if (edge.toId != edge.fromId) {
  15479. dx = (edge.to.x - edge.from.x);
  15480. dy = (edge.to.y - edge.from.y);
  15481. length = Math.sqrt(dx * dx + dy * dy);
  15482. if (length < minLength) {
  15483. // first check which node is larger
  15484. var parentNode = edge.from;
  15485. var childNode = edge.to;
  15486. if (edge.to.mass > edge.from.mass) {
  15487. parentNode = edge.to;
  15488. childNode = edge.from;
  15489. }
  15490. if (childNode.dynamicEdgesLength == 1) {
  15491. this._addToCluster(parentNode,childNode,false);
  15492. }
  15493. else if (parentNode.dynamicEdgesLength == 1) {
  15494. this._addToCluster(childNode,parentNode,false);
  15495. }
  15496. }
  15497. }
  15498. }
  15499. }
  15500. }
  15501. },
  15502. /**
  15503. * This function forces the network to cluster all nodes with only one connecting edge to their
  15504. * connected node.
  15505. *
  15506. * @private
  15507. */
  15508. _forceClustersByZoom : function() {
  15509. for (var nodeId in this.nodes) {
  15510. // another node could have absorbed this child.
  15511. if (this.nodes.hasOwnProperty(nodeId)) {
  15512. var childNode = this.nodes[nodeId];
  15513. // the edges can be swallowed by another decrease
  15514. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  15515. var edge = childNode.dynamicEdges[0];
  15516. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  15517. // group to the largest node
  15518. if (childNode.id != parentNode.id) {
  15519. if (parentNode.mass > childNode.mass) {
  15520. this._addToCluster(parentNode,childNode,true);
  15521. }
  15522. else {
  15523. this._addToCluster(childNode,parentNode,true);
  15524. }
  15525. }
  15526. }
  15527. }
  15528. }
  15529. },
  15530. /**
  15531. * To keep the nodes of roughly equal size we normalize the cluster levels.
  15532. * This function clusters a node to its smallest connected neighbour.
  15533. *
  15534. * @param node
  15535. * @private
  15536. */
  15537. _clusterToSmallestNeighbour : function(node) {
  15538. var smallestNeighbour = -1;
  15539. var smallestNeighbourNode = null;
  15540. for (var i = 0; i < node.dynamicEdges.length; i++) {
  15541. if (node.dynamicEdges[i] !== undefined) {
  15542. var neighbour = null;
  15543. if (node.dynamicEdges[i].fromId != node.id) {
  15544. neighbour = node.dynamicEdges[i].from;
  15545. }
  15546. else if (node.dynamicEdges[i].toId != node.id) {
  15547. neighbour = node.dynamicEdges[i].to;
  15548. }
  15549. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  15550. smallestNeighbour = neighbour.clusterSessions.length;
  15551. smallestNeighbourNode = neighbour;
  15552. }
  15553. }
  15554. }
  15555. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  15556. this._addToCluster(neighbour, node, true);
  15557. }
  15558. },
  15559. /**
  15560. * This function forms clusters from hubs, it loops over all nodes
  15561. *
  15562. * @param {Boolean} force | Disregard zoom level
  15563. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  15564. * @private
  15565. */
  15566. _formClustersByHub : function(force, onlyEqual) {
  15567. // we loop over all nodes in the list
  15568. for (var nodeId in this.nodes) {
  15569. // we check if it is still available since it can be used by the clustering in this loop
  15570. if (this.nodes.hasOwnProperty(nodeId)) {
  15571. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  15572. }
  15573. }
  15574. },
  15575. /**
  15576. * This function forms a cluster from a specific preselected hub node
  15577. *
  15578. * @param {Node} hubNode | the node we will cluster as a hub
  15579. * @param {Boolean} force | Disregard zoom level
  15580. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  15581. * @param {Number} [absorptionSizeOffset] |
  15582. * @private
  15583. */
  15584. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  15585. if (absorptionSizeOffset === undefined) {
  15586. absorptionSizeOffset = 0;
  15587. }
  15588. // we decide if the node is a hub
  15589. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  15590. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  15591. // initialize variables
  15592. var dx,dy,length;
  15593. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  15594. var allowCluster = false;
  15595. // we create a list of edges because the dynamicEdges change over the course of this loop
  15596. var edgesIdarray = [];
  15597. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  15598. for (var j = 0; j < amountOfInitialEdges; j++) {
  15599. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  15600. }
  15601. // if the hub clustering is not forces, we check if one of the edges connected
  15602. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  15603. if (force == false) {
  15604. allowCluster = false;
  15605. for (j = 0; j < amountOfInitialEdges; j++) {
  15606. var edge = this.edges[edgesIdarray[j]];
  15607. if (edge !== undefined) {
  15608. if (edge.connected) {
  15609. if (edge.toId != edge.fromId) {
  15610. dx = (edge.to.x - edge.from.x);
  15611. dy = (edge.to.y - edge.from.y);
  15612. length = Math.sqrt(dx * dx + dy * dy);
  15613. if (length < minLength) {
  15614. allowCluster = true;
  15615. break;
  15616. }
  15617. }
  15618. }
  15619. }
  15620. }
  15621. }
  15622. // start the clustering if allowed
  15623. if ((!force && allowCluster) || force) {
  15624. // we loop over all edges INITIALLY connected to this hub
  15625. for (j = 0; j < amountOfInitialEdges; j++) {
  15626. edge = this.edges[edgesIdarray[j]];
  15627. // the edge can be clustered by this function in a previous loop
  15628. if (edge !== undefined) {
  15629. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  15630. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  15631. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  15632. (childNode.id != hubNode.id)) {
  15633. this._addToCluster(hubNode,childNode,force);
  15634. }
  15635. }
  15636. }
  15637. }
  15638. }
  15639. },
  15640. /**
  15641. * This function adds the child node to the parent node, creating a cluster if it is not already.
  15642. *
  15643. * @param {Node} parentNode | this is the node that will house the child node
  15644. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  15645. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  15646. * @private
  15647. */
  15648. _addToCluster : function(parentNode, childNode, force) {
  15649. // join child node in the parent node
  15650. parentNode.containedNodes[childNode.id] = childNode;
  15651. // manage all the edges connected to the child and parent nodes
  15652. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  15653. var edge = childNode.dynamicEdges[i];
  15654. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  15655. this._addToContainedEdges(parentNode,childNode,edge);
  15656. }
  15657. else {
  15658. this._connectEdgeToCluster(parentNode,childNode,edge);
  15659. }
  15660. }
  15661. // a contained node has no dynamic edges.
  15662. childNode.dynamicEdges = [];
  15663. // remove circular edges from clusters
  15664. this._containCircularEdgesFromNode(parentNode,childNode);
  15665. // remove the childNode from the global nodes object
  15666. delete this.nodes[childNode.id];
  15667. // update the properties of the child and parent
  15668. var massBefore = parentNode.mass;
  15669. childNode.clusterSession = this.clusterSession;
  15670. parentNode.mass += childNode.mass;
  15671. parentNode.clusterSize += childNode.clusterSize;
  15672. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  15673. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  15674. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  15675. parentNode.clusterSessions.push(this.clusterSession);
  15676. }
  15677. // forced clusters only open from screen size and double tap
  15678. if (force == true) {
  15679. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  15680. parentNode.formationScale = 0;
  15681. }
  15682. else {
  15683. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  15684. }
  15685. // recalculate the size of the node on the next time the node is rendered
  15686. parentNode.clearSizeCache();
  15687. // set the pop-out scale for the childnode
  15688. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  15689. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  15690. childNode.clearVelocity();
  15691. // the mass has altered, preservation of energy dictates the velocity to be updated
  15692. parentNode.updateVelocity(massBefore);
  15693. // restart the simulation to reorganise all nodes
  15694. this.moving = true;
  15695. },
  15696. /**
  15697. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  15698. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  15699. * It has to be called if a level is collapsed. It is called by _formClusters().
  15700. * @private
  15701. */
  15702. _updateDynamicEdges : function() {
  15703. for (var i = 0; i < this.nodeIndices.length; i++) {
  15704. var node = this.nodes[this.nodeIndices[i]];
  15705. node.dynamicEdgesLength = node.dynamicEdges.length;
  15706. // this corrects for multiple edges pointing at the same other node
  15707. var correction = 0;
  15708. if (node.dynamicEdgesLength > 1) {
  15709. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  15710. var edgeToId = node.dynamicEdges[j].toId;
  15711. var edgeFromId = node.dynamicEdges[j].fromId;
  15712. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  15713. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  15714. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  15715. correction += 1;
  15716. }
  15717. }
  15718. }
  15719. }
  15720. node.dynamicEdgesLength -= correction;
  15721. }
  15722. },
  15723. /**
  15724. * This adds an edge from the childNode to the contained edges of the parent node
  15725. *
  15726. * @param parentNode | Node object
  15727. * @param childNode | Node object
  15728. * @param edge | Edge object
  15729. * @private
  15730. */
  15731. _addToContainedEdges : function(parentNode, childNode, edge) {
  15732. // create an array object if it does not yet exist for this childNode
  15733. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  15734. parentNode.containedEdges[childNode.id] = []
  15735. }
  15736. // add this edge to the list
  15737. parentNode.containedEdges[childNode.id].push(edge);
  15738. // remove the edge from the global edges object
  15739. delete this.edges[edge.id];
  15740. // remove the edge from the parent object
  15741. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  15742. if (parentNode.dynamicEdges[i].id == edge.id) {
  15743. parentNode.dynamicEdges.splice(i,1);
  15744. break;
  15745. }
  15746. }
  15747. },
  15748. /**
  15749. * This function connects an edge that was connected to a child node to the parent node.
  15750. * It keeps track of which nodes it has been connected to with the originalId array.
  15751. *
  15752. * @param {Node} parentNode | Node object
  15753. * @param {Node} childNode | Node object
  15754. * @param {Edge} edge | Edge object
  15755. * @private
  15756. */
  15757. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  15758. // handle circular edges
  15759. if (edge.toId == edge.fromId) {
  15760. this._addToContainedEdges(parentNode, childNode, edge);
  15761. }
  15762. else {
  15763. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  15764. edge.originalToId.push(childNode.id);
  15765. edge.to = parentNode;
  15766. edge.toId = parentNode.id;
  15767. }
  15768. else { // edge connected to other node with the "from" side
  15769. edge.originalFromId.push(childNode.id);
  15770. edge.from = parentNode;
  15771. edge.fromId = parentNode.id;
  15772. }
  15773. this._addToReroutedEdges(parentNode,childNode,edge);
  15774. }
  15775. },
  15776. /**
  15777. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  15778. * these edges inside of the cluster.
  15779. *
  15780. * @param parentNode
  15781. * @param childNode
  15782. * @private
  15783. */
  15784. _containCircularEdgesFromNode : function(parentNode, childNode) {
  15785. // manage all the edges connected to the child and parent nodes
  15786. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  15787. var edge = parentNode.dynamicEdges[i];
  15788. // handle circular edges
  15789. if (edge.toId == edge.fromId) {
  15790. this._addToContainedEdges(parentNode, childNode, edge);
  15791. }
  15792. }
  15793. },
  15794. /**
  15795. * This adds an edge from the childNode to the rerouted edges of the parent node
  15796. *
  15797. * @param parentNode | Node object
  15798. * @param childNode | Node object
  15799. * @param edge | Edge object
  15800. * @private
  15801. */
  15802. _addToReroutedEdges : function(parentNode, childNode, edge) {
  15803. // create an array object if it does not yet exist for this childNode
  15804. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  15805. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  15806. parentNode.reroutedEdges[childNode.id] = [];
  15807. }
  15808. parentNode.reroutedEdges[childNode.id].push(edge);
  15809. // this edge becomes part of the dynamicEdges of the cluster node
  15810. parentNode.dynamicEdges.push(edge);
  15811. },
  15812. /**
  15813. * This function connects an edge that was connected to a cluster node back to the child node.
  15814. *
  15815. * @param parentNode | Node object
  15816. * @param childNode | Node object
  15817. * @private
  15818. */
  15819. _connectEdgeBackToChild : function(parentNode, childNode) {
  15820. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  15821. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  15822. var edge = parentNode.reroutedEdges[childNode.id][i];
  15823. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  15824. edge.originalFromId.pop();
  15825. edge.fromId = childNode.id;
  15826. edge.from = childNode;
  15827. }
  15828. else {
  15829. edge.originalToId.pop();
  15830. edge.toId = childNode.id;
  15831. edge.to = childNode;
  15832. }
  15833. // append this edge to the list of edges connecting to the childnode
  15834. childNode.dynamicEdges.push(edge);
  15835. // remove the edge from the parent object
  15836. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  15837. if (parentNode.dynamicEdges[j].id == edge.id) {
  15838. parentNode.dynamicEdges.splice(j,1);
  15839. break;
  15840. }
  15841. }
  15842. }
  15843. // remove the entry from the rerouted edges
  15844. delete parentNode.reroutedEdges[childNode.id];
  15845. }
  15846. },
  15847. /**
  15848. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  15849. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  15850. * parentNode
  15851. *
  15852. * @param parentNode | Node object
  15853. * @private
  15854. */
  15855. _validateEdges : function(parentNode) {
  15856. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  15857. var edge = parentNode.dynamicEdges[i];
  15858. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  15859. parentNode.dynamicEdges.splice(i,1);
  15860. }
  15861. }
  15862. },
  15863. /**
  15864. * This function released the contained edges back into the global domain and puts them back into the
  15865. * dynamic edges of both parent and child.
  15866. *
  15867. * @param {Node} parentNode |
  15868. * @param {Node} childNode |
  15869. * @private
  15870. */
  15871. _releaseContainedEdges : function(parentNode, childNode) {
  15872. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  15873. var edge = parentNode.containedEdges[childNode.id][i];
  15874. // put the edge back in the global edges object
  15875. this.edges[edge.id] = edge;
  15876. // put the edge back in the dynamic edges of the child and parent
  15877. childNode.dynamicEdges.push(edge);
  15878. parentNode.dynamicEdges.push(edge);
  15879. }
  15880. // remove the entry from the contained edges
  15881. delete parentNode.containedEdges[childNode.id];
  15882. },
  15883. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  15884. /**
  15885. * This updates the node labels for all nodes (for debugging purposes)
  15886. */
  15887. updateLabels : function() {
  15888. var nodeId;
  15889. // update node labels
  15890. for (nodeId in this.nodes) {
  15891. if (this.nodes.hasOwnProperty(nodeId)) {
  15892. var node = this.nodes[nodeId];
  15893. if (node.clusterSize > 1) {
  15894. node.label = "[".concat(String(node.clusterSize),"]");
  15895. }
  15896. }
  15897. }
  15898. // update node labels
  15899. for (nodeId in this.nodes) {
  15900. if (this.nodes.hasOwnProperty(nodeId)) {
  15901. node = this.nodes[nodeId];
  15902. if (node.clusterSize == 1) {
  15903. if (node.originalLabel !== undefined) {
  15904. node.label = node.originalLabel;
  15905. }
  15906. else {
  15907. node.label = String(node.id);
  15908. }
  15909. }
  15910. }
  15911. }
  15912. // /* Debug Override */
  15913. // for (nodeId in this.nodes) {
  15914. // if (this.nodes.hasOwnProperty(nodeId)) {
  15915. // node = this.nodes[nodeId];
  15916. // node.label = String(node.level);
  15917. // }
  15918. // }
  15919. },
  15920. /**
  15921. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  15922. * if the rest of the nodes are already a few cluster levels in.
  15923. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  15924. * clustered enough to the clusterToSmallestNeighbours function.
  15925. */
  15926. normalizeClusterLevels : function() {
  15927. var maxLevel = 0;
  15928. var minLevel = 1e9;
  15929. var clusterLevel = 0;
  15930. var nodeId;
  15931. // we loop over all nodes in the list
  15932. for (nodeId in this.nodes) {
  15933. if (this.nodes.hasOwnProperty(nodeId)) {
  15934. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  15935. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  15936. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  15937. }
  15938. }
  15939. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  15940. var amountOfNodes = this.nodeIndices.length;
  15941. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  15942. // we loop over all nodes in the list
  15943. for (nodeId in this.nodes) {
  15944. if (this.nodes.hasOwnProperty(nodeId)) {
  15945. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  15946. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  15947. }
  15948. }
  15949. }
  15950. this._updateNodeIndexList();
  15951. this._updateDynamicEdges();
  15952. // if a cluster was formed, we increase the clusterSession
  15953. if (this.nodeIndices.length != amountOfNodes) {
  15954. this.clusterSession += 1;
  15955. }
  15956. }
  15957. },
  15958. /**
  15959. * This function determines if the cluster we want to decluster is in the active area
  15960. * this means around the zoom center
  15961. *
  15962. * @param {Node} node
  15963. * @returns {boolean}
  15964. * @private
  15965. */
  15966. _nodeInActiveArea : function(node) {
  15967. return (
  15968. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  15969. &&
  15970. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  15971. )
  15972. },
  15973. /**
  15974. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  15975. * It puts large clusters away from the center and randomizes the order.
  15976. *
  15977. */
  15978. repositionNodes : function() {
  15979. for (var i = 0; i < this.nodeIndices.length; i++) {
  15980. var node = this.nodes[this.nodeIndices[i]];
  15981. if ((node.xFixed == false || node.yFixed == false)) {
  15982. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  15983. var angle = 2 * Math.PI * Math.random();
  15984. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15985. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15986. this._repositionBezierNodes(node);
  15987. }
  15988. }
  15989. },
  15990. /**
  15991. * We determine how many connections denote an important hub.
  15992. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  15993. *
  15994. * @private
  15995. */
  15996. _getHubSize : function() {
  15997. var average = 0;
  15998. var averageSquared = 0;
  15999. var hubCounter = 0;
  16000. var largestHub = 0;
  16001. for (var i = 0; i < this.nodeIndices.length; i++) {
  16002. var node = this.nodes[this.nodeIndices[i]];
  16003. if (node.dynamicEdgesLength > largestHub) {
  16004. largestHub = node.dynamicEdgesLength;
  16005. }
  16006. average += node.dynamicEdgesLength;
  16007. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  16008. hubCounter += 1;
  16009. }
  16010. average = average / hubCounter;
  16011. averageSquared = averageSquared / hubCounter;
  16012. var variance = averageSquared - Math.pow(average,2);
  16013. var standardDeviation = Math.sqrt(variance);
  16014. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  16015. // always have at least one to cluster
  16016. if (this.hubThreshold > largestHub) {
  16017. this.hubThreshold = largestHub;
  16018. }
  16019. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  16020. // console.log("hubThreshold:",this.hubThreshold);
  16021. },
  16022. /**
  16023. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  16024. * with this amount we can cluster specifically on these chains.
  16025. *
  16026. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  16027. * @private
  16028. */
  16029. _reduceAmountOfChains : function(fraction) {
  16030. this.hubThreshold = 2;
  16031. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  16032. for (var nodeId in this.nodes) {
  16033. if (this.nodes.hasOwnProperty(nodeId)) {
  16034. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  16035. if (reduceAmount > 0) {
  16036. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  16037. reduceAmount -= 1;
  16038. }
  16039. }
  16040. }
  16041. }
  16042. },
  16043. /**
  16044. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  16045. * with this amount we can cluster specifically on these chains.
  16046. *
  16047. * @private
  16048. */
  16049. _getChainFraction : function() {
  16050. var chains = 0;
  16051. var total = 0;
  16052. for (var nodeId in this.nodes) {
  16053. if (this.nodes.hasOwnProperty(nodeId)) {
  16054. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  16055. chains += 1;
  16056. }
  16057. total += 1;
  16058. }
  16059. }
  16060. return chains/total;
  16061. }
  16062. };
  16063. var SelectionMixin = {
  16064. /**
  16065. * This function can be called from the _doInAllSectors function
  16066. *
  16067. * @param object
  16068. * @param overlappingNodes
  16069. * @private
  16070. */
  16071. _getNodesOverlappingWith : function(object, overlappingNodes) {
  16072. var nodes = this.nodes;
  16073. for (var nodeId in nodes) {
  16074. if (nodes.hasOwnProperty(nodeId)) {
  16075. if (nodes[nodeId].isOverlappingWith(object)) {
  16076. overlappingNodes.push(nodeId);
  16077. }
  16078. }
  16079. }
  16080. },
  16081. /**
  16082. * retrieve all nodes overlapping with given object
  16083. * @param {Object} object An object with parameters left, top, right, bottom
  16084. * @return {Number[]} An array with id's of the overlapping nodes
  16085. * @private
  16086. */
  16087. _getAllNodesOverlappingWith : function (object) {
  16088. var overlappingNodes = [];
  16089. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  16090. return overlappingNodes;
  16091. },
  16092. /**
  16093. * Return a position object in canvasspace from a single point in screenspace
  16094. *
  16095. * @param pointer
  16096. * @returns {{left: number, top: number, right: number, bottom: number}}
  16097. * @private
  16098. */
  16099. _pointerToPositionObject : function(pointer) {
  16100. var x = this._XconvertDOMtoCanvas(pointer.x);
  16101. var y = this._YconvertDOMtoCanvas(pointer.y);
  16102. return {left: x,
  16103. top: y,
  16104. right: x,
  16105. bottom: y};
  16106. },
  16107. /**
  16108. * Get the top node at the a specific point (like a click)
  16109. *
  16110. * @param {{x: Number, y: Number}} pointer
  16111. * @return {Node | null} node
  16112. * @private
  16113. */
  16114. _getNodeAt : function (pointer) {
  16115. // we first check if this is an navigation controls element
  16116. var positionObject = this._pointerToPositionObject(pointer);
  16117. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  16118. // if there are overlapping nodes, select the last one, this is the
  16119. // one which is drawn on top of the others
  16120. if (overlappingNodes.length > 0) {
  16121. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  16122. }
  16123. else {
  16124. return null;
  16125. }
  16126. },
  16127. /**
  16128. * retrieve all edges overlapping with given object, selector is around center
  16129. * @param {Object} object An object with parameters left, top, right, bottom
  16130. * @return {Number[]} An array with id's of the overlapping nodes
  16131. * @private
  16132. */
  16133. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  16134. var edges = this.edges;
  16135. for (var edgeId in edges) {
  16136. if (edges.hasOwnProperty(edgeId)) {
  16137. if (edges[edgeId].isOverlappingWith(object)) {
  16138. overlappingEdges.push(edgeId);
  16139. }
  16140. }
  16141. }
  16142. },
  16143. /**
  16144. * retrieve all nodes overlapping with given object
  16145. * @param {Object} object An object with parameters left, top, right, bottom
  16146. * @return {Number[]} An array with id's of the overlapping nodes
  16147. * @private
  16148. */
  16149. _getAllEdgesOverlappingWith : function (object) {
  16150. var overlappingEdges = [];
  16151. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  16152. return overlappingEdges;
  16153. },
  16154. /**
  16155. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  16156. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  16157. *
  16158. * @param pointer
  16159. * @returns {null}
  16160. * @private
  16161. */
  16162. _getEdgeAt : function(pointer) {
  16163. var positionObject = this._pointerToPositionObject(pointer);
  16164. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  16165. if (overlappingEdges.length > 0) {
  16166. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  16167. }
  16168. else {
  16169. return null;
  16170. }
  16171. },
  16172. /**
  16173. * Add object to the selection array.
  16174. *
  16175. * @param obj
  16176. * @private
  16177. */
  16178. _addToSelection : function(obj) {
  16179. if (obj instanceof Node) {
  16180. this.selectionObj.nodes[obj.id] = obj;
  16181. }
  16182. else {
  16183. this.selectionObj.edges[obj.id] = obj;
  16184. }
  16185. },
  16186. /**
  16187. * Add object to the selection array.
  16188. *
  16189. * @param obj
  16190. * @private
  16191. */
  16192. _addToHover : function(obj) {
  16193. if (obj instanceof Node) {
  16194. this.hoverObj.nodes[obj.id] = obj;
  16195. }
  16196. else {
  16197. this.hoverObj.edges[obj.id] = obj;
  16198. }
  16199. },
  16200. /**
  16201. * Remove a single option from selection.
  16202. *
  16203. * @param {Object} obj
  16204. * @private
  16205. */
  16206. _removeFromSelection : function(obj) {
  16207. if (obj instanceof Node) {
  16208. delete this.selectionObj.nodes[obj.id];
  16209. }
  16210. else {
  16211. delete this.selectionObj.edges[obj.id];
  16212. }
  16213. },
  16214. /**
  16215. * Unselect all. The selectionObj is useful for this.
  16216. *
  16217. * @param {Boolean} [doNotTrigger] | ignore trigger
  16218. * @private
  16219. */
  16220. _unselectAll : function(doNotTrigger) {
  16221. if (doNotTrigger === undefined) {
  16222. doNotTrigger = false;
  16223. }
  16224. for(var nodeId in this.selectionObj.nodes) {
  16225. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16226. this.selectionObj.nodes[nodeId].unselect();
  16227. }
  16228. }
  16229. for(var edgeId in this.selectionObj.edges) {
  16230. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16231. this.selectionObj.edges[edgeId].unselect();
  16232. }
  16233. }
  16234. this.selectionObj = {nodes:{},edges:{}};
  16235. if (doNotTrigger == false) {
  16236. this.emit('select', this.getSelection());
  16237. }
  16238. },
  16239. /**
  16240. * Unselect all clusters. The selectionObj is useful for this.
  16241. *
  16242. * @param {Boolean} [doNotTrigger] | ignore trigger
  16243. * @private
  16244. */
  16245. _unselectClusters : function(doNotTrigger) {
  16246. if (doNotTrigger === undefined) {
  16247. doNotTrigger = false;
  16248. }
  16249. for (var nodeId in this.selectionObj.nodes) {
  16250. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16251. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  16252. this.selectionObj.nodes[nodeId].unselect();
  16253. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  16254. }
  16255. }
  16256. }
  16257. if (doNotTrigger == false) {
  16258. this.emit('select', this.getSelection());
  16259. }
  16260. },
  16261. /**
  16262. * return the number of selected nodes
  16263. *
  16264. * @returns {number}
  16265. * @private
  16266. */
  16267. _getSelectedNodeCount : function() {
  16268. var count = 0;
  16269. for (var nodeId in this.selectionObj.nodes) {
  16270. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16271. count += 1;
  16272. }
  16273. }
  16274. return count;
  16275. },
  16276. /**
  16277. * return the selected node
  16278. *
  16279. * @returns {number}
  16280. * @private
  16281. */
  16282. _getSelectedNode : function() {
  16283. for (var nodeId in this.selectionObj.nodes) {
  16284. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16285. return this.selectionObj.nodes[nodeId];
  16286. }
  16287. }
  16288. return null;
  16289. },
  16290. /**
  16291. * return the selected edge
  16292. *
  16293. * @returns {number}
  16294. * @private
  16295. */
  16296. _getSelectedEdge : function() {
  16297. for (var edgeId in this.selectionObj.edges) {
  16298. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16299. return this.selectionObj.edges[edgeId];
  16300. }
  16301. }
  16302. return null;
  16303. },
  16304. /**
  16305. * return the number of selected edges
  16306. *
  16307. * @returns {number}
  16308. * @private
  16309. */
  16310. _getSelectedEdgeCount : function() {
  16311. var count = 0;
  16312. for (var edgeId in this.selectionObj.edges) {
  16313. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16314. count += 1;
  16315. }
  16316. }
  16317. return count;
  16318. },
  16319. /**
  16320. * return the number of selected objects.
  16321. *
  16322. * @returns {number}
  16323. * @private
  16324. */
  16325. _getSelectedObjectCount : function() {
  16326. var count = 0;
  16327. for(var nodeId in this.selectionObj.nodes) {
  16328. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16329. count += 1;
  16330. }
  16331. }
  16332. for(var edgeId in this.selectionObj.edges) {
  16333. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16334. count += 1;
  16335. }
  16336. }
  16337. return count;
  16338. },
  16339. /**
  16340. * Check if anything is selected
  16341. *
  16342. * @returns {boolean}
  16343. * @private
  16344. */
  16345. _selectionIsEmpty : function() {
  16346. for(var nodeId in this.selectionObj.nodes) {
  16347. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16348. return false;
  16349. }
  16350. }
  16351. for(var edgeId in this.selectionObj.edges) {
  16352. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16353. return false;
  16354. }
  16355. }
  16356. return true;
  16357. },
  16358. /**
  16359. * check if one of the selected nodes is a cluster.
  16360. *
  16361. * @returns {boolean}
  16362. * @private
  16363. */
  16364. _clusterInSelection : function() {
  16365. for(var nodeId in this.selectionObj.nodes) {
  16366. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16367. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  16368. return true;
  16369. }
  16370. }
  16371. }
  16372. return false;
  16373. },
  16374. /**
  16375. * select the edges connected to the node that is being selected
  16376. *
  16377. * @param {Node} node
  16378. * @private
  16379. */
  16380. _selectConnectedEdges : function(node) {
  16381. for (var i = 0; i < node.dynamicEdges.length; i++) {
  16382. var edge = node.dynamicEdges[i];
  16383. edge.select();
  16384. this._addToSelection(edge);
  16385. }
  16386. },
  16387. /**
  16388. * select the edges connected to the node that is being selected
  16389. *
  16390. * @param {Node} node
  16391. * @private
  16392. */
  16393. _hoverConnectedEdges : function(node) {
  16394. for (var i = 0; i < node.dynamicEdges.length; i++) {
  16395. var edge = node.dynamicEdges[i];
  16396. edge.hover = true;
  16397. this._addToHover(edge);
  16398. }
  16399. },
  16400. /**
  16401. * unselect the edges connected to the node that is being selected
  16402. *
  16403. * @param {Node} node
  16404. * @private
  16405. */
  16406. _unselectConnectedEdges : function(node) {
  16407. for (var i = 0; i < node.dynamicEdges.length; i++) {
  16408. var edge = node.dynamicEdges[i];
  16409. edge.unselect();
  16410. this._removeFromSelection(edge);
  16411. }
  16412. },
  16413. /**
  16414. * This is called when someone clicks on a node. either select or deselect it.
  16415. * If there is an existing selection and we don't want to append to it, clear the existing selection
  16416. *
  16417. * @param {Node || Edge} object
  16418. * @param {Boolean} append
  16419. * @param {Boolean} [doNotTrigger] | ignore trigger
  16420. * @private
  16421. */
  16422. _selectObject : function(object, append, doNotTrigger, highlightEdges) {
  16423. if (doNotTrigger === undefined) {
  16424. doNotTrigger = false;
  16425. }
  16426. if (highlightEdges === undefined) {
  16427. highlightEdges = true;
  16428. }
  16429. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  16430. this._unselectAll(true);
  16431. }
  16432. if (object.selected == false) {
  16433. object.select();
  16434. this._addToSelection(object);
  16435. if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
  16436. this._selectConnectedEdges(object);
  16437. }
  16438. }
  16439. else {
  16440. object.unselect();
  16441. this._removeFromSelection(object);
  16442. }
  16443. if (doNotTrigger == false) {
  16444. this.emit('select', this.getSelection());
  16445. }
  16446. },
  16447. /**
  16448. * This is called when someone clicks on a node. either select or deselect it.
  16449. * If there is an existing selection and we don't want to append to it, clear the existing selection
  16450. *
  16451. * @param {Node || Edge} object
  16452. * @private
  16453. */
  16454. _blurObject : function(object) {
  16455. if (object.hover == true) {
  16456. object.hover = false;
  16457. this.emit("blurNode",{node:object.id});
  16458. }
  16459. },
  16460. /**
  16461. * This is called when someone clicks on a node. either select or deselect it.
  16462. * If there is an existing selection and we don't want to append to it, clear the existing selection
  16463. *
  16464. * @param {Node || Edge} object
  16465. * @private
  16466. */
  16467. _hoverObject : function(object) {
  16468. if (object.hover == false) {
  16469. object.hover = true;
  16470. this._addToHover(object);
  16471. if (object instanceof Node) {
  16472. this.emit("hoverNode",{node:object.id});
  16473. }
  16474. }
  16475. if (object instanceof Node) {
  16476. this._hoverConnectedEdges(object);
  16477. }
  16478. },
  16479. /**
  16480. * handles the selection part of the touch, only for navigation controls elements;
  16481. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  16482. * This is the most responsive solution
  16483. *
  16484. * @param {Object} pointer
  16485. * @private
  16486. */
  16487. _handleTouch : function(pointer) {
  16488. },
  16489. /**
  16490. * handles the selection part of the tap;
  16491. *
  16492. * @param {Object} pointer
  16493. * @private
  16494. */
  16495. _handleTap : function(pointer) {
  16496. var node = this._getNodeAt(pointer);
  16497. if (node != null) {
  16498. this._selectObject(node,false);
  16499. }
  16500. else {
  16501. var edge = this._getEdgeAt(pointer);
  16502. if (edge != null) {
  16503. this._selectObject(edge,false);
  16504. }
  16505. else {
  16506. this._unselectAll();
  16507. }
  16508. }
  16509. this.emit("click", this.getSelection());
  16510. this._redraw();
  16511. },
  16512. /**
  16513. * handles the selection part of the double tap and opens a cluster if needed
  16514. *
  16515. * @param {Object} pointer
  16516. * @private
  16517. */
  16518. _handleDoubleTap : function(pointer) {
  16519. var node = this._getNodeAt(pointer);
  16520. if (node != null && node !== undefined) {
  16521. // we reset the areaCenter here so the opening of the node will occur
  16522. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  16523. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  16524. this.openCluster(node);
  16525. }
  16526. this.emit("doubleClick", this.getSelection());
  16527. },
  16528. /**
  16529. * Handle the onHold selection part
  16530. *
  16531. * @param pointer
  16532. * @private
  16533. */
  16534. _handleOnHold : function(pointer) {
  16535. var node = this._getNodeAt(pointer);
  16536. if (node != null) {
  16537. this._selectObject(node,true);
  16538. }
  16539. else {
  16540. var edge = this._getEdgeAt(pointer);
  16541. if (edge != null) {
  16542. this._selectObject(edge,true);
  16543. }
  16544. }
  16545. this._redraw();
  16546. },
  16547. /**
  16548. * handle the onRelease event. These functions are here for the navigation controls module.
  16549. *
  16550. * @private
  16551. */
  16552. _handleOnRelease : function(pointer) {
  16553. },
  16554. /**
  16555. *
  16556. * retrieve the currently selected objects
  16557. * @return {Number[] | String[]} selection An array with the ids of the
  16558. * selected nodes.
  16559. */
  16560. getSelection : function() {
  16561. var nodeIds = this.getSelectedNodes();
  16562. var edgeIds = this.getSelectedEdges();
  16563. return {nodes:nodeIds, edges:edgeIds};
  16564. },
  16565. /**
  16566. *
  16567. * retrieve the currently selected nodes
  16568. * @return {String} selection An array with the ids of the
  16569. * selected nodes.
  16570. */
  16571. getSelectedNodes : function() {
  16572. var idArray = [];
  16573. for(var nodeId in this.selectionObj.nodes) {
  16574. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16575. idArray.push(nodeId);
  16576. }
  16577. }
  16578. return idArray
  16579. },
  16580. /**
  16581. *
  16582. * retrieve the currently selected edges
  16583. * @return {Array} selection An array with the ids of the
  16584. * selected nodes.
  16585. */
  16586. getSelectedEdges : function() {
  16587. var idArray = [];
  16588. for(var edgeId in this.selectionObj.edges) {
  16589. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16590. idArray.push(edgeId);
  16591. }
  16592. }
  16593. return idArray;
  16594. },
  16595. /**
  16596. * select zero or more nodes
  16597. * @param {Number[] | String[]} selection An array with the ids of the
  16598. * selected nodes.
  16599. */
  16600. setSelection : function(selection) {
  16601. var i, iMax, id;
  16602. if (!selection || (selection.length == undefined))
  16603. throw 'Selection must be an array with ids';
  16604. // first unselect any selected node
  16605. this._unselectAll(true);
  16606. for (i = 0, iMax = selection.length; i < iMax; i++) {
  16607. id = selection[i];
  16608. var node = this.nodes[id];
  16609. if (!node) {
  16610. throw new RangeError('Node with id "' + id + '" not found');
  16611. }
  16612. this._selectObject(node,true,true);
  16613. }
  16614. console.log("setSelection is deprecated. Please use selectNodes instead.")
  16615. this.redraw();
  16616. },
  16617. /**
  16618. * select zero or more nodes with the option to highlight edges
  16619. * @param {Number[] | String[]} selection An array with the ids of the
  16620. * selected nodes.
  16621. * @param {boolean} [highlightEdges]
  16622. */
  16623. selectNodes : function(selection, highlightEdges) {
  16624. var i, iMax, id;
  16625. if (!selection || (selection.length == undefined))
  16626. throw 'Selection must be an array with ids';
  16627. // first unselect any selected node
  16628. this._unselectAll(true);
  16629. for (i = 0, iMax = selection.length; i < iMax; i++) {
  16630. id = selection[i];
  16631. var node = this.nodes[id];
  16632. if (!node) {
  16633. throw new RangeError('Node with id "' + id + '" not found');
  16634. }
  16635. this._selectObject(node,true,true,highlightEdges);
  16636. }
  16637. this.redraw();
  16638. },
  16639. /**
  16640. * select zero or more edges
  16641. * @param {Number[] | String[]} selection An array with the ids of the
  16642. * selected nodes.
  16643. */
  16644. selectEdges : function(selection) {
  16645. var i, iMax, id;
  16646. if (!selection || (selection.length == undefined))
  16647. throw 'Selection must be an array with ids';
  16648. // first unselect any selected node
  16649. this._unselectAll(true);
  16650. for (i = 0, iMax = selection.length; i < iMax; i++) {
  16651. id = selection[i];
  16652. var edge = this.edges[id];
  16653. if (!edge) {
  16654. throw new RangeError('Edge with id "' + id + '" not found');
  16655. }
  16656. this._selectObject(edge,true,true,highlightEdges);
  16657. }
  16658. this.redraw();
  16659. },
  16660. /**
  16661. * Validate the selection: remove ids of nodes which no longer exist
  16662. * @private
  16663. */
  16664. _updateSelection : function () {
  16665. for(var nodeId in this.selectionObj.nodes) {
  16666. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  16667. if (!this.nodes.hasOwnProperty(nodeId)) {
  16668. delete this.selectionObj.nodes[nodeId];
  16669. }
  16670. }
  16671. }
  16672. for(var edgeId in this.selectionObj.edges) {
  16673. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  16674. if (!this.edges.hasOwnProperty(edgeId)) {
  16675. delete this.selectionObj.edges[edgeId];
  16676. }
  16677. }
  16678. }
  16679. }
  16680. };
  16681. /**
  16682. * Created by Alex on 1/22/14.
  16683. */
  16684. var NavigationMixin = {
  16685. _cleanNavigation : function() {
  16686. // clean up previosu navigation items
  16687. var wrapper = document.getElementById('network-navigation_wrapper');
  16688. if (wrapper != null) {
  16689. this.containerElement.removeChild(wrapper);
  16690. }
  16691. document.onmouseup = null;
  16692. },
  16693. /**
  16694. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  16695. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  16696. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  16697. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  16698. *
  16699. * @private
  16700. */
  16701. _loadNavigationElements : function() {
  16702. this._cleanNavigation();
  16703. this.navigationDivs = {};
  16704. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  16705. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  16706. this.navigationDivs['wrapper'] = document.createElement('div');
  16707. this.navigationDivs['wrapper'].id = "network-navigation_wrapper";
  16708. this.navigationDivs['wrapper'].style.position = "absolute";
  16709. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  16710. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  16711. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  16712. for (var i = 0; i < navigationDivs.length; i++) {
  16713. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  16714. this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
  16715. this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
  16716. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  16717. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  16718. }
  16719. document.onmouseup = this._stopMovement.bind(this);
  16720. },
  16721. /**
  16722. * this stops all movement induced by the navigation buttons
  16723. *
  16724. * @private
  16725. */
  16726. _stopMovement : function() {
  16727. this._xStopMoving();
  16728. this._yStopMoving();
  16729. this._stopZoom();
  16730. },
  16731. /**
  16732. * stops the actions performed by page up and down etc.
  16733. *
  16734. * @param event
  16735. * @private
  16736. */
  16737. _preventDefault : function(event) {
  16738. if (event !== undefined) {
  16739. if (event.preventDefault) {
  16740. event.preventDefault();
  16741. } else {
  16742. event.returnValue = false;
  16743. }
  16744. }
  16745. },
  16746. /**
  16747. * move the screen up
  16748. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  16749. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  16750. * To avoid this behaviour, we do the translation in the start loop.
  16751. *
  16752. * @private
  16753. */
  16754. _moveUp : function(event) {
  16755. this.yIncrement = this.constants.keyboard.speed.y;
  16756. this.start(); // if there is no node movement, the calculation wont be done
  16757. this._preventDefault(event);
  16758. if (this.navigationDivs) {
  16759. this.navigationDivs['up'].className += " active";
  16760. }
  16761. },
  16762. /**
  16763. * move the screen down
  16764. * @private
  16765. */
  16766. _moveDown : function(event) {
  16767. this.yIncrement = -this.constants.keyboard.speed.y;
  16768. this.start(); // if there is no node movement, the calculation wont be done
  16769. this._preventDefault(event);
  16770. if (this.navigationDivs) {
  16771. this.navigationDivs['down'].className += " active";
  16772. }
  16773. },
  16774. /**
  16775. * move the screen left
  16776. * @private
  16777. */
  16778. _moveLeft : function(event) {
  16779. this.xIncrement = this.constants.keyboard.speed.x;
  16780. this.start(); // if there is no node movement, the calculation wont be done
  16781. this._preventDefault(event);
  16782. if (this.navigationDivs) {
  16783. this.navigationDivs['left'].className += " active";
  16784. }
  16785. },
  16786. /**
  16787. * move the screen right
  16788. * @private
  16789. */
  16790. _moveRight : function(event) {
  16791. this.xIncrement = -this.constants.keyboard.speed.y;
  16792. this.start(); // if there is no node movement, the calculation wont be done
  16793. this._preventDefault(event);
  16794. if (this.navigationDivs) {
  16795. this.navigationDivs['right'].className += " active";
  16796. }
  16797. },
  16798. /**
  16799. * Zoom in, using the same method as the movement.
  16800. * @private
  16801. */
  16802. _zoomIn : function(event) {
  16803. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  16804. this.start(); // if there is no node movement, the calculation wont be done
  16805. this._preventDefault(event);
  16806. if (this.navigationDivs) {
  16807. this.navigationDivs['zoomIn'].className += " active";
  16808. }
  16809. },
  16810. /**
  16811. * Zoom out
  16812. * @private
  16813. */
  16814. _zoomOut : function() {
  16815. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  16816. this.start(); // if there is no node movement, the calculation wont be done
  16817. this._preventDefault(event);
  16818. if (this.navigationDivs) {
  16819. this.navigationDivs['zoomOut'].className += " active";
  16820. }
  16821. },
  16822. /**
  16823. * Stop zooming and unhighlight the zoom controls
  16824. * @private
  16825. */
  16826. _stopZoom : function() {
  16827. this.zoomIncrement = 0;
  16828. if (this.navigationDivs) {
  16829. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  16830. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  16831. }
  16832. },
  16833. /**
  16834. * Stop moving in the Y direction and unHighlight the up and down
  16835. * @private
  16836. */
  16837. _yStopMoving : function() {
  16838. this.yIncrement = 0;
  16839. if (this.navigationDivs) {
  16840. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  16841. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  16842. }
  16843. },
  16844. /**
  16845. * Stop moving in the X direction and unHighlight left and right.
  16846. * @private
  16847. */
  16848. _xStopMoving : function() {
  16849. this.xIncrement = 0;
  16850. if (this.navigationDivs) {
  16851. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  16852. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  16853. }
  16854. }
  16855. };
  16856. /**
  16857. * Created by Alex on 2/10/14.
  16858. */
  16859. var networkMixinLoaders = {
  16860. /**
  16861. * Load a mixin into the network object
  16862. *
  16863. * @param {Object} sourceVariable | this object has to contain functions.
  16864. * @private
  16865. */
  16866. _loadMixin: function (sourceVariable) {
  16867. for (var mixinFunction in sourceVariable) {
  16868. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  16869. Network.prototype[mixinFunction] = sourceVariable[mixinFunction];
  16870. }
  16871. }
  16872. },
  16873. /**
  16874. * removes a mixin from the network object.
  16875. *
  16876. * @param {Object} sourceVariable | this object has to contain functions.
  16877. * @private
  16878. */
  16879. _clearMixin: function (sourceVariable) {
  16880. for (var mixinFunction in sourceVariable) {
  16881. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  16882. Network.prototype[mixinFunction] = undefined;
  16883. }
  16884. }
  16885. },
  16886. /**
  16887. * Mixin the physics system and initialize the parameters required.
  16888. *
  16889. * @private
  16890. */
  16891. _loadPhysicsSystem: function () {
  16892. this._loadMixin(physicsMixin);
  16893. this._loadSelectedForceSolver();
  16894. if (this.constants.configurePhysics == true) {
  16895. this._loadPhysicsConfiguration();
  16896. }
  16897. },
  16898. /**
  16899. * Mixin the cluster system and initialize the parameters required.
  16900. *
  16901. * @private
  16902. */
  16903. _loadClusterSystem: function () {
  16904. this.clusterSession = 0;
  16905. this.hubThreshold = 5;
  16906. this._loadMixin(ClusterMixin);
  16907. },
  16908. /**
  16909. * Mixin the sector system and initialize the parameters required
  16910. *
  16911. * @private
  16912. */
  16913. _loadSectorSystem: function () {
  16914. this.sectors = {};
  16915. this.activeSector = ["default"];
  16916. this.sectors["active"] = {};
  16917. this.sectors["active"]["default"] = {"nodes": {},
  16918. "edges": {},
  16919. "nodeIndices": [],
  16920. "formationScale": 1.0,
  16921. "drawingNode": undefined };
  16922. this.sectors["frozen"] = {};
  16923. this.sectors["support"] = {"nodes": {},
  16924. "edges": {},
  16925. "nodeIndices": [],
  16926. "formationScale": 1.0,
  16927. "drawingNode": undefined };
  16928. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  16929. this._loadMixin(SectorMixin);
  16930. },
  16931. /**
  16932. * Mixin the selection system and initialize the parameters required
  16933. *
  16934. * @private
  16935. */
  16936. _loadSelectionSystem: function () {
  16937. this.selectionObj = {nodes: {}, edges: {}};
  16938. this._loadMixin(SelectionMixin);
  16939. },
  16940. /**
  16941. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  16942. *
  16943. * @private
  16944. */
  16945. _loadManipulationSystem: function () {
  16946. // reset global variables -- these are used by the selection of nodes and edges.
  16947. this.blockConnectingEdgeSelection = false;
  16948. this.forceAppendSelection = false;
  16949. if (this.constants.dataManipulation.enabled == true) {
  16950. // load the manipulator HTML elements. All styling done in css.
  16951. if (this.manipulationDiv === undefined) {
  16952. this.manipulationDiv = document.createElement('div');
  16953. this.manipulationDiv.className = 'network-manipulationDiv';
  16954. this.manipulationDiv.id = 'network-manipulationDiv';
  16955. if (this.editMode == true) {
  16956. this.manipulationDiv.style.display = "block";
  16957. }
  16958. else {
  16959. this.manipulationDiv.style.display = "none";
  16960. }
  16961. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  16962. }
  16963. if (this.editModeDiv === undefined) {
  16964. this.editModeDiv = document.createElement('div');
  16965. this.editModeDiv.className = 'network-manipulation-editMode';
  16966. this.editModeDiv.id = 'network-manipulation-editMode';
  16967. if (this.editMode == true) {
  16968. this.editModeDiv.style.display = "none";
  16969. }
  16970. else {
  16971. this.editModeDiv.style.display = "block";
  16972. }
  16973. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  16974. }
  16975. if (this.closeDiv === undefined) {
  16976. this.closeDiv = document.createElement('div');
  16977. this.closeDiv.className = 'network-manipulation-closeDiv';
  16978. this.closeDiv.id = 'network-manipulation-closeDiv';
  16979. this.closeDiv.style.display = this.manipulationDiv.style.display;
  16980. this.containerElement.insertBefore(this.closeDiv, this.frame);
  16981. }
  16982. // load the manipulation functions
  16983. this._loadMixin(manipulationMixin);
  16984. // create the manipulator toolbar
  16985. this._createManipulatorBar();
  16986. }
  16987. else {
  16988. if (this.manipulationDiv !== undefined) {
  16989. // removes all the bindings and overloads
  16990. this._createManipulatorBar();
  16991. // remove the manipulation divs
  16992. this.containerElement.removeChild(this.manipulationDiv);
  16993. this.containerElement.removeChild(this.editModeDiv);
  16994. this.containerElement.removeChild(this.closeDiv);
  16995. this.manipulationDiv = undefined;
  16996. this.editModeDiv = undefined;
  16997. this.closeDiv = undefined;
  16998. // remove the mixin functions
  16999. this._clearMixin(manipulationMixin);
  17000. }
  17001. }
  17002. },
  17003. /**
  17004. * Mixin the navigation (User Interface) system and initialize the parameters required
  17005. *
  17006. * @private
  17007. */
  17008. _loadNavigationControls: function () {
  17009. this._loadMixin(NavigationMixin);
  17010. // the clean function removes the button divs, this is done to remove the bindings.
  17011. this._cleanNavigation();
  17012. if (this.constants.navigation.enabled == true) {
  17013. this._loadNavigationElements();
  17014. }
  17015. },
  17016. /**
  17017. * Mixin the hierarchical layout system.
  17018. *
  17019. * @private
  17020. */
  17021. _loadHierarchySystem: function () {
  17022. this._loadMixin(HierarchicalLayoutMixin);
  17023. }
  17024. };
  17025. /**
  17026. * @constructor Network
  17027. * Create a network visualization, displaying nodes and edges.
  17028. *
  17029. * @param {Element} container The DOM element in which the Network will
  17030. * be created. Normally a div element.
  17031. * @param {Object} data An object containing parameters
  17032. * {Array} nodes
  17033. * {Array} edges
  17034. * @param {Object} options Options
  17035. */
  17036. function Network (container, data, options) {
  17037. if (!(this instanceof Network)) {
  17038. throw new SyntaxError('Constructor must be called with the new operator');
  17039. }
  17040. this._initializeMixinLoaders();
  17041. // create variables and set default values
  17042. this.containerElement = container;
  17043. this.width = '100%';
  17044. this.height = '100%';
  17045. // render and calculation settings
  17046. this.renderRefreshRate = 60; // hz (fps)
  17047. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  17048. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  17049. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  17050. this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
  17051. this.stabilize = true; // stabilize before displaying the network
  17052. this.selectable = true;
  17053. this.initializing = true;
  17054. // these functions are triggered when the dataset is edited
  17055. this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
  17056. // set constant values
  17057. this.constants = {
  17058. nodes: {
  17059. radiusMin: 5,
  17060. radiusMax: 20,
  17061. radius: 5,
  17062. shape: 'ellipse',
  17063. image: undefined,
  17064. widthMin: 16, // px
  17065. widthMax: 64, // px
  17066. fixed: false,
  17067. fontColor: 'black',
  17068. fontSize: 14, // px
  17069. fontFace: 'verdana',
  17070. level: -1,
  17071. color: {
  17072. border: '#2B7CE9',
  17073. background: '#97C2FC',
  17074. highlight: {
  17075. border: '#2B7CE9',
  17076. background: '#D2E5FF'
  17077. },
  17078. hover: {
  17079. border: '#2B7CE9',
  17080. background: '#D2E5FF'
  17081. }
  17082. },
  17083. borderColor: '#2B7CE9',
  17084. backgroundColor: '#97C2FC',
  17085. highlightColor: '#D2E5FF',
  17086. group: undefined
  17087. },
  17088. edges: {
  17089. widthMin: 1,
  17090. widthMax: 15,
  17091. width: 1,
  17092. widthSelectionMultiplier: 2,
  17093. hoverWidth: 1.5,
  17094. style: 'line',
  17095. color: {
  17096. color:'#848484',
  17097. highlight:'#848484',
  17098. hover: '#848484'
  17099. },
  17100. fontColor: '#343434',
  17101. fontSize: 14, // px
  17102. fontFace: 'arial',
  17103. fontFill: 'white',
  17104. arrowScaleFactor: 1,
  17105. dash: {
  17106. length: 10,
  17107. gap: 5,
  17108. altLength: undefined
  17109. }
  17110. },
  17111. configurePhysics:false,
  17112. physics: {
  17113. barnesHut: {
  17114. enabled: true,
  17115. theta: 1 / 0.6, // inverted to save time during calculation
  17116. gravitationalConstant: -2000,
  17117. centralGravity: 0.3,
  17118. springLength: 95,
  17119. springConstant: 0.04,
  17120. damping: 0.09
  17121. },
  17122. repulsion: {
  17123. centralGravity: 0.1,
  17124. springLength: 200,
  17125. springConstant: 0.05,
  17126. nodeDistance: 100,
  17127. damping: 0.09
  17128. },
  17129. hierarchicalRepulsion: {
  17130. enabled: false,
  17131. centralGravity: 0.5,
  17132. springLength: 150,
  17133. springConstant: 0.01,
  17134. nodeDistance: 60,
  17135. damping: 0.09
  17136. },
  17137. damping: null,
  17138. centralGravity: null,
  17139. springLength: null,
  17140. springConstant: null
  17141. },
  17142. clustering: { // Per Node in Cluster = PNiC
  17143. enabled: false, // (Boolean) | global on/off switch for clustering.
  17144. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  17145. 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
  17146. 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
  17147. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  17148. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  17149. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  17150. 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.
  17151. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  17152. maxFontSize: 1000,
  17153. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  17154. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  17155. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  17156. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  17157. height: 1, // (px PNiC) | growth of the height per node in cluster.
  17158. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  17159. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  17160. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  17161. clusterLevelDifference: 2
  17162. },
  17163. navigation: {
  17164. enabled: false
  17165. },
  17166. keyboard: {
  17167. enabled: false,
  17168. speed: {x: 10, y: 10, zoom: 0.02}
  17169. },
  17170. dataManipulation: {
  17171. enabled: false,
  17172. initiallyVisible: false
  17173. },
  17174. hierarchicalLayout: {
  17175. enabled:false,
  17176. levelSeparation: 150,
  17177. nodeSpacing: 100,
  17178. direction: "UD" // UD, DU, LR, RL
  17179. },
  17180. freezeForStabilization: false,
  17181. smoothCurves: true,
  17182. maxVelocity: 10,
  17183. minVelocity: 0.1, // px/s
  17184. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  17185. labels:{
  17186. add:"Add Node",
  17187. edit:"Edit",
  17188. link:"Add Link",
  17189. del:"Delete selected",
  17190. editNode:"Edit Node",
  17191. editEdge:"Edit Edge",
  17192. back:"Back",
  17193. addDescription:"Click in an empty space to place a new node.",
  17194. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  17195. editEdgeDescription:"Click on the control points and drag them to a node to connect to it.",
  17196. addError:"The function for add does not support two arguments (data,callback).",
  17197. linkError:"The function for connect does not support two arguments (data,callback).",
  17198. editError:"The function for edit does not support two arguments (data, callback).",
  17199. editBoundError:"No edit function has been bound to this button.",
  17200. deleteError:"The function for delete does not support two arguments (data, callback).",
  17201. deleteClusterError:"Clusters cannot be deleted."
  17202. },
  17203. tooltip: {
  17204. delay: 300,
  17205. fontColor: 'black',
  17206. fontSize: 14, // px
  17207. fontFace: 'verdana',
  17208. color: {
  17209. border: '#666',
  17210. background: '#FFFFC6'
  17211. }
  17212. },
  17213. dragNetwork: true,
  17214. dragNodes: true,
  17215. zoomable: true,
  17216. hover: false
  17217. };
  17218. this.hoverObj = {nodes:{},edges:{}};
  17219. // Node variables
  17220. var network = this;
  17221. this.groups = new Groups(); // object with groups
  17222. this.images = new Images(); // object with images
  17223. this.images.setOnloadCallback(function () {
  17224. network._redraw();
  17225. });
  17226. // keyboard navigation variables
  17227. this.xIncrement = 0;
  17228. this.yIncrement = 0;
  17229. this.zoomIncrement = 0;
  17230. // loading all the mixins:
  17231. // load the force calculation functions, grouped under the physics system.
  17232. this._loadPhysicsSystem();
  17233. // create a frame and canvas
  17234. this._create();
  17235. // load the sector system. (mandatory, fully integrated with Network)
  17236. this._loadSectorSystem();
  17237. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  17238. this._loadClusterSystem();
  17239. // load the selection system. (mandatory, required by Network)
  17240. this._loadSelectionSystem();
  17241. // load the selection system. (mandatory, required by Network)
  17242. this._loadHierarchySystem();
  17243. // apply options
  17244. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  17245. this._setScale(1);
  17246. this.setOptions(options);
  17247. // other vars
  17248. this.freezeSimulation = false;// freeze the simulation
  17249. this.cachedFunctions = {};
  17250. // containers for nodes and edges
  17251. this.calculationNodes = {};
  17252. this.calculationNodeIndices = [];
  17253. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  17254. this.nodes = {}; // object with Node objects
  17255. this.edges = {}; // object with Edge objects
  17256. // position and scale variables and objects
  17257. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  17258. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  17259. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  17260. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  17261. this.scale = 1; // defining the global scale variable in the constructor
  17262. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  17263. // datasets or dataviews
  17264. this.nodesData = null; // A DataSet or DataView
  17265. this.edgesData = null; // A DataSet or DataView
  17266. // create event listeners used to subscribe on the DataSets of the nodes and edges
  17267. this.nodesListeners = {
  17268. 'add': function (event, params) {
  17269. network._addNodes(params.items);
  17270. network.start();
  17271. },
  17272. 'update': function (event, params) {
  17273. network._updateNodes(params.items);
  17274. network.start();
  17275. },
  17276. 'remove': function (event, params) {
  17277. network._removeNodes(params.items);
  17278. network.start();
  17279. }
  17280. };
  17281. this.edgesListeners = {
  17282. 'add': function (event, params) {
  17283. network._addEdges(params.items);
  17284. network.start();
  17285. },
  17286. 'update': function (event, params) {
  17287. network._updateEdges(params.items);
  17288. network.start();
  17289. },
  17290. 'remove': function (event, params) {
  17291. network._removeEdges(params.items);
  17292. network.start();
  17293. }
  17294. };
  17295. // properties for the animation
  17296. this.moving = true;
  17297. this.timer = undefined; // Scheduling function. Is definded in this.start();
  17298. // load data (the disable start variable will be the same as the enabled clustering)
  17299. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  17300. // hierarchical layout
  17301. this.initializing = false;
  17302. if (this.constants.hierarchicalLayout.enabled == true) {
  17303. this._setupHierarchicalLayout();
  17304. }
  17305. else {
  17306. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  17307. if (this.stabilize == false) {
  17308. this.zoomExtent(true,this.constants.clustering.enabled);
  17309. }
  17310. }
  17311. // if clustering is disabled, the simulation will have started in the setData function
  17312. if (this.constants.clustering.enabled) {
  17313. this.startWithClustering();
  17314. }
  17315. }
  17316. // Extend Network with an Emitter mixin
  17317. Emitter(Network.prototype);
  17318. /**
  17319. * Get the script path where the vis.js library is located
  17320. *
  17321. * @returns {string | null} path Path or null when not found. Path does not
  17322. * end with a slash.
  17323. * @private
  17324. */
  17325. Network.prototype._getScriptPath = function() {
  17326. var scripts = document.getElementsByTagName( 'script' );
  17327. // find script named vis.js or vis.min.js
  17328. for (var i = 0; i < scripts.length; i++) {
  17329. var src = scripts[i].src;
  17330. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  17331. if (match) {
  17332. // return path without the script name
  17333. return src.substring(0, src.length - match[0].length);
  17334. }
  17335. }
  17336. return null;
  17337. };
  17338. /**
  17339. * Find the center position of the network
  17340. * @private
  17341. */
  17342. Network.prototype._getRange = function() {
  17343. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  17344. for (var nodeId in this.nodes) {
  17345. if (this.nodes.hasOwnProperty(nodeId)) {
  17346. node = this.nodes[nodeId];
  17347. if (minX > (node.x)) {minX = node.x;}
  17348. if (maxX < (node.x)) {maxX = node.x;}
  17349. if (minY > (node.y)) {minY = node.y;}
  17350. if (maxY < (node.y)) {maxY = node.y;}
  17351. }
  17352. }
  17353. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  17354. minY = 0, maxY = 0, minX = 0, maxX = 0;
  17355. }
  17356. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  17357. };
  17358. /**
  17359. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  17360. * @returns {{x: number, y: number}}
  17361. * @private
  17362. */
  17363. Network.prototype._findCenter = function(range) {
  17364. return {x: (0.5 * (range.maxX + range.minX)),
  17365. y: (0.5 * (range.maxY + range.minY))};
  17366. };
  17367. /**
  17368. * center the network
  17369. *
  17370. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  17371. */
  17372. Network.prototype._centerNetwork = function(range) {
  17373. var center = this._findCenter(range);
  17374. center.x *= this.scale;
  17375. center.y *= this.scale;
  17376. center.x -= 0.5 * this.frame.canvas.clientWidth;
  17377. center.y -= 0.5 * this.frame.canvas.clientHeight;
  17378. this._setTranslation(-center.x,-center.y); // set at 0,0
  17379. };
  17380. /**
  17381. * This function zooms out to fit all data on screen based on amount of nodes
  17382. *
  17383. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  17384. * @param {Boolean} [disableStart] | If true, start is not called.
  17385. */
  17386. Network.prototype.zoomExtent = function(initialZoom, disableStart) {
  17387. if (initialZoom === undefined) {
  17388. initialZoom = false;
  17389. }
  17390. if (disableStart === undefined) {
  17391. disableStart = false;
  17392. }
  17393. var range = this._getRange();
  17394. var zoomLevel;
  17395. if (initialZoom == true) {
  17396. var numberOfNodes = this.nodeIndices.length;
  17397. if (this.constants.smoothCurves == true) {
  17398. if (this.constants.clustering.enabled == true &&
  17399. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  17400. 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.
  17401. }
  17402. else {
  17403. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  17404. }
  17405. }
  17406. else {
  17407. if (this.constants.clustering.enabled == true &&
  17408. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  17409. 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.
  17410. }
  17411. else {
  17412. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  17413. }
  17414. }
  17415. // correct for larger canvasses.
  17416. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  17417. zoomLevel *= factor;
  17418. }
  17419. else {
  17420. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  17421. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  17422. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  17423. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  17424. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  17425. }
  17426. if (zoomLevel > 1.0) {
  17427. zoomLevel = 1.0;
  17428. }
  17429. this._setScale(zoomLevel);
  17430. this._centerNetwork(range);
  17431. if (disableStart == false) {
  17432. this.moving = true;
  17433. this.start();
  17434. }
  17435. };
  17436. /**
  17437. * Update the this.nodeIndices with the most recent node index list
  17438. * @private
  17439. */
  17440. Network.prototype._updateNodeIndexList = function() {
  17441. this._clearNodeIndexList();
  17442. for (var idx in this.nodes) {
  17443. if (this.nodes.hasOwnProperty(idx)) {
  17444. this.nodeIndices.push(idx);
  17445. }
  17446. }
  17447. };
  17448. /**
  17449. * Set nodes and edges, and optionally options as well.
  17450. *
  17451. * @param {Object} data Object containing parameters:
  17452. * {Array | DataSet | DataView} [nodes] Array with nodes
  17453. * {Array | DataSet | DataView} [edges] Array with edges
  17454. * {String} [dot] String containing data in DOT format
  17455. * {Options} [options] Object with options
  17456. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  17457. */
  17458. Network.prototype.setData = function(data, disableStart) {
  17459. if (disableStart === undefined) {
  17460. disableStart = false;
  17461. }
  17462. if (data && data.dot && (data.nodes || data.edges)) {
  17463. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  17464. ' parameter pair "nodes" and "edges", but not both.');
  17465. }
  17466. // set options
  17467. this.setOptions(data && data.options);
  17468. // set all data
  17469. if (data && data.dot) {
  17470. // parse DOT file
  17471. if(data && data.dot) {
  17472. var dotData = vis.util.DOTToGraph(data.dot);
  17473. this.setData(dotData);
  17474. return;
  17475. }
  17476. }
  17477. else {
  17478. this._setNodes(data && data.nodes);
  17479. this._setEdges(data && data.edges);
  17480. }
  17481. this._putDataInSector();
  17482. if (!disableStart) {
  17483. // find a stable position or start animating to a stable position
  17484. if (this.stabilize) {
  17485. var me = this;
  17486. setTimeout(function() {me._stabilize(); me.start();},0)
  17487. }
  17488. else {
  17489. this.start();
  17490. }
  17491. }
  17492. };
  17493. /**
  17494. * Set options
  17495. * @param {Object} options
  17496. * @param {Boolean} [initializeView] | set zoom and translation to default.
  17497. */
  17498. Network.prototype.setOptions = function (options) {
  17499. if (options) {
  17500. var prop;
  17501. // retrieve parameter values
  17502. if (options.width !== undefined) {this.width = options.width;}
  17503. if (options.height !== undefined) {this.height = options.height;}
  17504. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  17505. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  17506. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  17507. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  17508. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  17509. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  17510. if (options.dragNetwork !== undefined) {this.constants.dragNetwork = options.dragNetwork;}
  17511. if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;}
  17512. if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
  17513. if (options.hover !== undefined) {this.constants.hover = options.hover;}
  17514. // TODO: deprecated since version 3.0.0. Cleanup some day
  17515. if (options.dragGraph !== undefined) {
  17516. throw new Error('Option dragGraph is renamed to dragNetwork');
  17517. }
  17518. if (options.labels !== undefined) {
  17519. for (prop in options.labels) {
  17520. if (options.labels.hasOwnProperty(prop)) {
  17521. this.constants.labels[prop] = options.labels[prop];
  17522. }
  17523. }
  17524. }
  17525. if (options.onAdd) {
  17526. this.triggerFunctions.add = options.onAdd;
  17527. }
  17528. if (options.onEdit) {
  17529. this.triggerFunctions.edit = options.onEdit;
  17530. }
  17531. if (options.onEditEdge) {
  17532. this.triggerFunctions.editEdge = options.onEditEdge;
  17533. }
  17534. if (options.onConnect) {
  17535. this.triggerFunctions.connect = options.onConnect;
  17536. }
  17537. if (options.onDelete) {
  17538. this.triggerFunctions.del = options.onDelete;
  17539. }
  17540. if (options.physics) {
  17541. if (options.physics.barnesHut) {
  17542. this.constants.physics.barnesHut.enabled = true;
  17543. for (prop in options.physics.barnesHut) {
  17544. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  17545. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  17546. }
  17547. }
  17548. }
  17549. if (options.physics.repulsion) {
  17550. this.constants.physics.barnesHut.enabled = false;
  17551. for (prop in options.physics.repulsion) {
  17552. if (options.physics.repulsion.hasOwnProperty(prop)) {
  17553. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  17554. }
  17555. }
  17556. }
  17557. if (options.physics.hierarchicalRepulsion) {
  17558. this.constants.hierarchicalLayout.enabled = true;
  17559. this.constants.physics.hierarchicalRepulsion.enabled = true;
  17560. this.constants.physics.barnesHut.enabled = false;
  17561. for (prop in options.physics.hierarchicalRepulsion) {
  17562. if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
  17563. this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
  17564. }
  17565. }
  17566. }
  17567. }
  17568. if (options.hierarchicalLayout) {
  17569. this.constants.hierarchicalLayout.enabled = true;
  17570. for (prop in options.hierarchicalLayout) {
  17571. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  17572. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  17573. }
  17574. }
  17575. }
  17576. else if (options.hierarchicalLayout !== undefined) {
  17577. this.constants.hierarchicalLayout.enabled = false;
  17578. }
  17579. if (options.clustering) {
  17580. this.constants.clustering.enabled = true;
  17581. for (prop in options.clustering) {
  17582. if (options.clustering.hasOwnProperty(prop)) {
  17583. this.constants.clustering[prop] = options.clustering[prop];
  17584. }
  17585. }
  17586. }
  17587. else if (options.clustering !== undefined) {
  17588. this.constants.clustering.enabled = false;
  17589. }
  17590. if (options.navigation) {
  17591. this.constants.navigation.enabled = true;
  17592. for (prop in options.navigation) {
  17593. if (options.navigation.hasOwnProperty(prop)) {
  17594. this.constants.navigation[prop] = options.navigation[prop];
  17595. }
  17596. }
  17597. }
  17598. else if (options.navigation !== undefined) {
  17599. this.constants.navigation.enabled = false;
  17600. }
  17601. if (options.keyboard) {
  17602. this.constants.keyboard.enabled = true;
  17603. for (prop in options.keyboard) {
  17604. if (options.keyboard.hasOwnProperty(prop)) {
  17605. this.constants.keyboard[prop] = options.keyboard[prop];
  17606. }
  17607. }
  17608. }
  17609. else if (options.keyboard !== undefined) {
  17610. this.constants.keyboard.enabled = false;
  17611. }
  17612. if (options.dataManipulation) {
  17613. this.constants.dataManipulation.enabled = true;
  17614. for (prop in options.dataManipulation) {
  17615. if (options.dataManipulation.hasOwnProperty(prop)) {
  17616. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  17617. }
  17618. }
  17619. this.editMode = this.constants.dataManipulation.initiallyVisible;
  17620. }
  17621. else if (options.dataManipulation !== undefined) {
  17622. this.constants.dataManipulation.enabled = false;
  17623. }
  17624. // TODO: work out these options and document them
  17625. if (options.edges) {
  17626. for (prop in options.edges) {
  17627. if (options.edges.hasOwnProperty(prop)) {
  17628. if (typeof options.edges[prop] != "object") {
  17629. this.constants.edges[prop] = options.edges[prop];
  17630. }
  17631. }
  17632. }
  17633. if (options.edges.color !== undefined) {
  17634. if (util.isString(options.edges.color)) {
  17635. this.constants.edges.color = {};
  17636. this.constants.edges.color.color = options.edges.color;
  17637. this.constants.edges.color.highlight = options.edges.color;
  17638. this.constants.edges.color.hover = options.edges.color;
  17639. }
  17640. else {
  17641. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  17642. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  17643. if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
  17644. }
  17645. }
  17646. if (!options.edges.fontColor) {
  17647. if (options.edges.color !== undefined) {
  17648. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  17649. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  17650. }
  17651. }
  17652. // Added to support dashed lines
  17653. // David Jordan
  17654. // 2012-08-08
  17655. if (options.edges.dash) {
  17656. if (options.edges.dash.length !== undefined) {
  17657. this.constants.edges.dash.length = options.edges.dash.length;
  17658. }
  17659. if (options.edges.dash.gap !== undefined) {
  17660. this.constants.edges.dash.gap = options.edges.dash.gap;
  17661. }
  17662. if (options.edges.dash.altLength !== undefined) {
  17663. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  17664. }
  17665. }
  17666. }
  17667. if (options.nodes) {
  17668. for (prop in options.nodes) {
  17669. if (options.nodes.hasOwnProperty(prop)) {
  17670. this.constants.nodes[prop] = options.nodes[prop];
  17671. }
  17672. }
  17673. if (options.nodes.color) {
  17674. this.constants.nodes.color = util.parseColor(options.nodes.color);
  17675. }
  17676. /*
  17677. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  17678. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  17679. */
  17680. }
  17681. if (options.groups) {
  17682. for (var groupname in options.groups) {
  17683. if (options.groups.hasOwnProperty(groupname)) {
  17684. var group = options.groups[groupname];
  17685. this.groups.add(groupname, group);
  17686. }
  17687. }
  17688. }
  17689. if (options.tooltip) {
  17690. for (prop in options.tooltip) {
  17691. if (options.tooltip.hasOwnProperty(prop)) {
  17692. this.constants.tooltip[prop] = options.tooltip[prop];
  17693. }
  17694. }
  17695. if (options.tooltip.color) {
  17696. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  17697. }
  17698. }
  17699. }
  17700. // (Re)loading the mixins that can be enabled or disabled in the options.
  17701. // load the force calculation functions, grouped under the physics system.
  17702. this._loadPhysicsSystem();
  17703. // load the navigation system.
  17704. this._loadNavigationControls();
  17705. // load the data manipulation system
  17706. this._loadManipulationSystem();
  17707. // configure the smooth curves
  17708. this._configureSmoothCurves();
  17709. // bind keys. If disabled, this will not do anything;
  17710. this._createKeyBinds();
  17711. this.setSize(this.width, this.height);
  17712. this.moving = true;
  17713. this.start();
  17714. };
  17715. /**
  17716. * Create the main frame for the Network.
  17717. * This function is executed once when a Network object is created. The frame
  17718. * contains a canvas, and this canvas contains all objects like the axis and
  17719. * nodes.
  17720. * @private
  17721. */
  17722. Network.prototype._create = function () {
  17723. // remove all elements from the container element.
  17724. while (this.containerElement.hasChildNodes()) {
  17725. this.containerElement.removeChild(this.containerElement.firstChild);
  17726. }
  17727. this.frame = document.createElement('div');
  17728. this.frame.className = 'network-frame';
  17729. this.frame.style.position = 'relative';
  17730. this.frame.style.overflow = 'hidden';
  17731. // create the network canvas (HTML canvas element)
  17732. this.frame.canvas = document.createElement( 'canvas' );
  17733. this.frame.canvas.style.position = 'relative';
  17734. this.frame.appendChild(this.frame.canvas);
  17735. if (!this.frame.canvas.getContext) {
  17736. var noCanvas = document.createElement( 'DIV' );
  17737. noCanvas.style.color = 'red';
  17738. noCanvas.style.fontWeight = 'bold' ;
  17739. noCanvas.style.padding = '10px';
  17740. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  17741. this.frame.canvas.appendChild(noCanvas);
  17742. }
  17743. var me = this;
  17744. this.drag = {};
  17745. this.pinch = {};
  17746. this.hammer = Hammer(this.frame.canvas, {
  17747. prevent_default: true
  17748. });
  17749. this.hammer.on('tap', me._onTap.bind(me) );
  17750. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  17751. this.hammer.on('hold', me._onHold.bind(me) );
  17752. this.hammer.on('pinch', me._onPinch.bind(me) );
  17753. this.hammer.on('touch', me._onTouch.bind(me) );
  17754. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  17755. this.hammer.on('drag', me._onDrag.bind(me) );
  17756. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  17757. this.hammer.on('release', me._onRelease.bind(me) );
  17758. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  17759. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  17760. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  17761. // add the frame to the container element
  17762. this.containerElement.appendChild(this.frame);
  17763. };
  17764. /**
  17765. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  17766. * @private
  17767. */
  17768. Network.prototype._createKeyBinds = function() {
  17769. var me = this;
  17770. this.mousetrap = mousetrap;
  17771. this.mousetrap.reset();
  17772. if (this.constants.keyboard.enabled == true) {
  17773. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  17774. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  17775. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  17776. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  17777. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  17778. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  17779. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  17780. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  17781. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  17782. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  17783. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  17784. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  17785. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  17786. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  17787. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  17788. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  17789. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  17790. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  17791. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  17792. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  17793. }
  17794. if (this.constants.dataManipulation.enabled == true) {
  17795. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  17796. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  17797. }
  17798. };
  17799. /**
  17800. * Get the pointer location from a touch location
  17801. * @param {{pageX: Number, pageY: Number}} touch
  17802. * @return {{x: Number, y: Number}} pointer
  17803. * @private
  17804. */
  17805. Network.prototype._getPointer = function (touch) {
  17806. return {
  17807. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  17808. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  17809. };
  17810. };
  17811. /**
  17812. * On start of a touch gesture, store the pointer
  17813. * @param event
  17814. * @private
  17815. */
  17816. Network.prototype._onTouch = function (event) {
  17817. this.drag.pointer = this._getPointer(event.gesture.center);
  17818. this.drag.pinched = false;
  17819. this.pinch.scale = this._getScale();
  17820. this._handleTouch(this.drag.pointer);
  17821. };
  17822. /**
  17823. * handle drag start event
  17824. * @private
  17825. */
  17826. Network.prototype._onDragStart = function () {
  17827. this._handleDragStart();
  17828. };
  17829. /**
  17830. * This function is called by _onDragStart.
  17831. * It is separated out because we can then overload it for the datamanipulation system.
  17832. *
  17833. * @private
  17834. */
  17835. Network.prototype._handleDragStart = function() {
  17836. var drag = this.drag;
  17837. var node = this._getNodeAt(drag.pointer);
  17838. // note: drag.pointer is set in _onTouch to get the initial touch location
  17839. drag.dragging = true;
  17840. drag.selection = [];
  17841. drag.translation = this._getTranslation();
  17842. drag.nodeId = null;
  17843. if (node != null) {
  17844. drag.nodeId = node.id;
  17845. // select the clicked node if not yet selected
  17846. if (!node.isSelected()) {
  17847. this._selectObject(node,false);
  17848. }
  17849. // create an array with the selected nodes and their original location and status
  17850. for (var objectId in this.selectionObj.nodes) {
  17851. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  17852. var object = this.selectionObj.nodes[objectId];
  17853. var s = {
  17854. id: object.id,
  17855. node: object,
  17856. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  17857. x: object.x,
  17858. y: object.y,
  17859. xFixed: object.xFixed,
  17860. yFixed: object.yFixed
  17861. };
  17862. object.xFixed = true;
  17863. object.yFixed = true;
  17864. drag.selection.push(s);
  17865. }
  17866. }
  17867. }
  17868. };
  17869. /**
  17870. * handle drag event
  17871. * @private
  17872. */
  17873. Network.prototype._onDrag = function (event) {
  17874. this._handleOnDrag(event)
  17875. };
  17876. /**
  17877. * This function is called by _onDrag.
  17878. * It is separated out because we can then overload it for the datamanipulation system.
  17879. *
  17880. * @private
  17881. */
  17882. Network.prototype._handleOnDrag = function(event) {
  17883. if (this.drag.pinched) {
  17884. return;
  17885. }
  17886. var pointer = this._getPointer(event.gesture.center);
  17887. var me = this,
  17888. drag = this.drag,
  17889. selection = drag.selection;
  17890. if (selection && selection.length && this.constants.dragNodes == true) {
  17891. // calculate delta's and new location
  17892. var deltaX = pointer.x - drag.pointer.x,
  17893. deltaY = pointer.y - drag.pointer.y;
  17894. // update position of all selected nodes
  17895. selection.forEach(function (s) {
  17896. var node = s.node;
  17897. if (!s.xFixed) {
  17898. node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
  17899. }
  17900. if (!s.yFixed) {
  17901. node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
  17902. }
  17903. });
  17904. // start _animationStep if not yet running
  17905. if (!this.moving) {
  17906. this.moving = true;
  17907. this.start();
  17908. }
  17909. }
  17910. else {
  17911. if (this.constants.dragNetwork == true) {
  17912. // move the network
  17913. var diffX = pointer.x - this.drag.pointer.x;
  17914. var diffY = pointer.y - this.drag.pointer.y;
  17915. this._setTranslation(
  17916. this.drag.translation.x + diffX,
  17917. this.drag.translation.y + diffY);
  17918. this._redraw();
  17919. this.moving = true;
  17920. this.start();
  17921. }
  17922. }
  17923. };
  17924. /**
  17925. * handle drag start event
  17926. * @private
  17927. */
  17928. Network.prototype._onDragEnd = function () {
  17929. this.drag.dragging = false;
  17930. var selection = this.drag.selection;
  17931. if (selection) {
  17932. selection.forEach(function (s) {
  17933. // restore original xFixed and yFixed
  17934. s.node.xFixed = s.xFixed;
  17935. s.node.yFixed = s.yFixed;
  17936. });
  17937. }
  17938. };
  17939. /**
  17940. * handle tap/click event: select/unselect a node
  17941. * @private
  17942. */
  17943. Network.prototype._onTap = function (event) {
  17944. var pointer = this._getPointer(event.gesture.center);
  17945. this.pointerPosition = pointer;
  17946. this._handleTap(pointer);
  17947. };
  17948. /**
  17949. * handle doubletap event
  17950. * @private
  17951. */
  17952. Network.prototype._onDoubleTap = function (event) {
  17953. var pointer = this._getPointer(event.gesture.center);
  17954. this._handleDoubleTap(pointer);
  17955. };
  17956. /**
  17957. * handle long tap event: multi select nodes
  17958. * @private
  17959. */
  17960. Network.prototype._onHold = function (event) {
  17961. var pointer = this._getPointer(event.gesture.center);
  17962. this.pointerPosition = pointer;
  17963. this._handleOnHold(pointer);
  17964. };
  17965. /**
  17966. * handle the release of the screen
  17967. *
  17968. * @private
  17969. */
  17970. Network.prototype._onRelease = function (event) {
  17971. var pointer = this._getPointer(event.gesture.center);
  17972. this._handleOnRelease(pointer);
  17973. };
  17974. /**
  17975. * Handle pinch event
  17976. * @param event
  17977. * @private
  17978. */
  17979. Network.prototype._onPinch = function (event) {
  17980. var pointer = this._getPointer(event.gesture.center);
  17981. this.drag.pinched = true;
  17982. if (!('scale' in this.pinch)) {
  17983. this.pinch.scale = 1;
  17984. }
  17985. // TODO: enabled moving while pinching?
  17986. var scale = this.pinch.scale * event.gesture.scale;
  17987. this._zoom(scale, pointer)
  17988. };
  17989. /**
  17990. * Zoom the network in or out
  17991. * @param {Number} scale a number around 1, and between 0.01 and 10
  17992. * @param {{x: Number, y: Number}} pointer Position on screen
  17993. * @return {Number} appliedScale scale is limited within the boundaries
  17994. * @private
  17995. */
  17996. Network.prototype._zoom = function(scale, pointer) {
  17997. if (this.constants.zoomable == true) {
  17998. var scaleOld = this._getScale();
  17999. if (scale < 0.00001) {
  18000. scale = 0.00001;
  18001. }
  18002. if (scale > 10) {
  18003. scale = 10;
  18004. }
  18005. // + this.frame.canvas.clientHeight / 2
  18006. var translation = this._getTranslation();
  18007. var scaleFrac = scale / scaleOld;
  18008. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  18009. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  18010. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  18011. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  18012. this._setScale(scale);
  18013. this._setTranslation(tx, ty);
  18014. this.updateClustersDefault();
  18015. this._redraw();
  18016. if (scaleOld < scale) {
  18017. this.emit("zoom", {direction:"+"});
  18018. }
  18019. else {
  18020. this.emit("zoom", {direction:"-"});
  18021. }
  18022. return scale;
  18023. }
  18024. };
  18025. /**
  18026. * Event handler for mouse wheel event, used to zoom the timeline
  18027. * See http://adomas.org/javascript-mouse-wheel/
  18028. * https://github.com/EightMedia/hammer.js/issues/256
  18029. * @param {MouseEvent} event
  18030. * @private
  18031. */
  18032. Network.prototype._onMouseWheel = function(event) {
  18033. // retrieve delta
  18034. var delta = 0;
  18035. if (event.wheelDelta) { /* IE/Opera. */
  18036. delta = event.wheelDelta/120;
  18037. } else if (event.detail) { /* Mozilla case. */
  18038. // In Mozilla, sign of delta is different than in IE.
  18039. // Also, delta is multiple of 3.
  18040. delta = -event.detail/3;
  18041. }
  18042. // If delta is nonzero, handle it.
  18043. // Basically, delta is now positive if wheel was scrolled up,
  18044. // and negative, if wheel was scrolled down.
  18045. if (delta) {
  18046. // calculate the new scale
  18047. var scale = this._getScale();
  18048. var zoom = delta / 10;
  18049. if (delta < 0) {
  18050. zoom = zoom / (1 - zoom);
  18051. }
  18052. scale *= (1 + zoom);
  18053. // calculate the pointer location
  18054. var gesture = util.fakeGesture(this, event);
  18055. var pointer = this._getPointer(gesture.center);
  18056. // apply the new scale
  18057. this._zoom(scale, pointer);
  18058. }
  18059. // Prevent default actions caused by mouse wheel.
  18060. event.preventDefault();
  18061. };
  18062. /**
  18063. * Mouse move handler for checking whether the title moves over a node with a title.
  18064. * @param {Event} event
  18065. * @private
  18066. */
  18067. Network.prototype._onMouseMoveTitle = function (event) {
  18068. var gesture = util.fakeGesture(this, event);
  18069. var pointer = this._getPointer(gesture.center);
  18070. // check if the previously selected node is still selected
  18071. if (this.popupObj) {
  18072. this._checkHidePopup(pointer);
  18073. }
  18074. // start a timeout that will check if the mouse is positioned above
  18075. // an element
  18076. var me = this;
  18077. var checkShow = function() {
  18078. me._checkShowPopup(pointer);
  18079. };
  18080. if (this.popupTimer) {
  18081. clearInterval(this.popupTimer); // stop any running calculationTimer
  18082. }
  18083. if (!this.drag.dragging) {
  18084. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  18085. }
  18086. /**
  18087. * Adding hover highlights
  18088. */
  18089. if (this.constants.hover == true) {
  18090. // removing all hover highlights
  18091. for (var edgeId in this.hoverObj.edges) {
  18092. if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  18093. this.hoverObj.edges[edgeId].hover = false;
  18094. delete this.hoverObj.edges[edgeId];
  18095. }
  18096. }
  18097. // adding hover highlights
  18098. var obj = this._getNodeAt(pointer);
  18099. if (obj == null) {
  18100. obj = this._getEdgeAt(pointer);
  18101. }
  18102. if (obj != null) {
  18103. this._hoverObject(obj);
  18104. }
  18105. // removing all node hover highlights except for the selected one.
  18106. for (var nodeId in this.hoverObj.nodes) {
  18107. if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  18108. if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
  18109. this._blurObject(this.hoverObj.nodes[nodeId]);
  18110. delete this.hoverObj.nodes[nodeId];
  18111. }
  18112. }
  18113. }
  18114. this.redraw();
  18115. }
  18116. };
  18117. /**
  18118. * Check if there is an element on the given position in the network
  18119. * (a node or edge). If so, and if this element has a title,
  18120. * show a popup window with its title.
  18121. *
  18122. * @param {{x:Number, y:Number}} pointer
  18123. * @private
  18124. */
  18125. Network.prototype._checkShowPopup = function (pointer) {
  18126. var obj = {
  18127. left: this._XconvertDOMtoCanvas(pointer.x),
  18128. top: this._YconvertDOMtoCanvas(pointer.y),
  18129. right: this._XconvertDOMtoCanvas(pointer.x),
  18130. bottom: this._YconvertDOMtoCanvas(pointer.y)
  18131. };
  18132. var id;
  18133. var lastPopupNode = this.popupObj;
  18134. if (this.popupObj == undefined) {
  18135. // search the nodes for overlap, select the top one in case of multiple nodes
  18136. var nodes = this.nodes;
  18137. for (id in nodes) {
  18138. if (nodes.hasOwnProperty(id)) {
  18139. var node = nodes[id];
  18140. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  18141. this.popupObj = node;
  18142. break;
  18143. }
  18144. }
  18145. }
  18146. }
  18147. if (this.popupObj === undefined) {
  18148. // search the edges for overlap
  18149. var edges = this.edges;
  18150. for (id in edges) {
  18151. if (edges.hasOwnProperty(id)) {
  18152. var edge = edges[id];
  18153. if (edge.connected && (edge.getTitle() !== undefined) &&
  18154. edge.isOverlappingWith(obj)) {
  18155. this.popupObj = edge;
  18156. break;
  18157. }
  18158. }
  18159. }
  18160. }
  18161. if (this.popupObj) {
  18162. // show popup message window
  18163. if (this.popupObj != lastPopupNode) {
  18164. var me = this;
  18165. if (!me.popup) {
  18166. me.popup = new Popup(me.frame, me.constants.tooltip);
  18167. }
  18168. // adjust a small offset such that the mouse cursor is located in the
  18169. // bottom left location of the popup, and you can easily move over the
  18170. // popup area
  18171. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  18172. me.popup.setText(me.popupObj.getTitle());
  18173. me.popup.show();
  18174. }
  18175. }
  18176. else {
  18177. if (this.popup) {
  18178. this.popup.hide();
  18179. }
  18180. }
  18181. };
  18182. /**
  18183. * Check if the popup must be hided, which is the case when the mouse is no
  18184. * longer hovering on the object
  18185. * @param {{x:Number, y:Number}} pointer
  18186. * @private
  18187. */
  18188. Network.prototype._checkHidePopup = function (pointer) {
  18189. if (!this.popupObj || !this._getNodeAt(pointer) ) {
  18190. this.popupObj = undefined;
  18191. if (this.popup) {
  18192. this.popup.hide();
  18193. }
  18194. }
  18195. };
  18196. /**
  18197. * Set a new size for the network
  18198. * @param {string} width Width in pixels or percentage (for example '800px'
  18199. * or '50%')
  18200. * @param {string} height Height in pixels or percentage (for example '400px'
  18201. * or '30%')
  18202. */
  18203. Network.prototype.setSize = function(width, height) {
  18204. this.frame.style.width = width;
  18205. this.frame.style.height = height;
  18206. this.frame.canvas.style.width = '100%';
  18207. this.frame.canvas.style.height = '100%';
  18208. this.frame.canvas.width = this.frame.canvas.clientWidth;
  18209. this.frame.canvas.height = this.frame.canvas.clientHeight;
  18210. if (this.manipulationDiv !== undefined) {
  18211. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  18212. }
  18213. if (this.navigationDivs !== undefined) {
  18214. if (this.navigationDivs['wrapper'] !== undefined) {
  18215. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  18216. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  18217. }
  18218. }
  18219. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  18220. };
  18221. /**
  18222. * Set a data set with nodes for the network
  18223. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  18224. * @private
  18225. */
  18226. Network.prototype._setNodes = function(nodes) {
  18227. var oldNodesData = this.nodesData;
  18228. if (nodes instanceof DataSet || nodes instanceof DataView) {
  18229. this.nodesData = nodes;
  18230. }
  18231. else if (nodes instanceof Array) {
  18232. this.nodesData = new DataSet();
  18233. this.nodesData.add(nodes);
  18234. }
  18235. else if (!nodes) {
  18236. this.nodesData = new DataSet();
  18237. }
  18238. else {
  18239. throw new TypeError('Array or DataSet expected');
  18240. }
  18241. if (oldNodesData) {
  18242. // unsubscribe from old dataset
  18243. util.forEach(this.nodesListeners, function (callback, event) {
  18244. oldNodesData.off(event, callback);
  18245. });
  18246. }
  18247. // remove drawn nodes
  18248. this.nodes = {};
  18249. if (this.nodesData) {
  18250. // subscribe to new dataset
  18251. var me = this;
  18252. util.forEach(this.nodesListeners, function (callback, event) {
  18253. me.nodesData.on(event, callback);
  18254. });
  18255. // draw all new nodes
  18256. var ids = this.nodesData.getIds();
  18257. this._addNodes(ids);
  18258. }
  18259. this._updateSelection();
  18260. };
  18261. /**
  18262. * Add nodes
  18263. * @param {Number[] | String[]} ids
  18264. * @private
  18265. */
  18266. Network.prototype._addNodes = function(ids) {
  18267. var id;
  18268. for (var i = 0, len = ids.length; i < len; i++) {
  18269. id = ids[i];
  18270. var data = this.nodesData.get(id);
  18271. var node = new Node(data, this.images, this.groups, this.constants);
  18272. this.nodes[id] = node; // note: this may replace an existing node
  18273. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  18274. var radius = 10 * 0.1*ids.length;
  18275. var angle = 2 * Math.PI * Math.random();
  18276. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  18277. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  18278. }
  18279. this.moving = true;
  18280. }
  18281. this._updateNodeIndexList();
  18282. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18283. this._resetLevels();
  18284. this._setupHierarchicalLayout();
  18285. }
  18286. this._updateCalculationNodes();
  18287. this._reconnectEdges();
  18288. this._updateValueRange(this.nodes);
  18289. this.updateLabels();
  18290. };
  18291. /**
  18292. * Update existing nodes, or create them when not yet existing
  18293. * @param {Number[] | String[]} ids
  18294. * @private
  18295. */
  18296. Network.prototype._updateNodes = function(ids) {
  18297. var nodes = this.nodes,
  18298. nodesData = this.nodesData;
  18299. for (var i = 0, len = ids.length; i < len; i++) {
  18300. var id = ids[i];
  18301. var node = nodes[id];
  18302. var data = nodesData.get(id);
  18303. if (node) {
  18304. // update node
  18305. node.setProperties(data, this.constants);
  18306. }
  18307. else {
  18308. // create node
  18309. node = new Node(properties, this.images, this.groups, this.constants);
  18310. nodes[id] = node;
  18311. }
  18312. }
  18313. this.moving = true;
  18314. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18315. this._resetLevels();
  18316. this._setupHierarchicalLayout();
  18317. }
  18318. this._updateNodeIndexList();
  18319. this._reconnectEdges();
  18320. this._updateValueRange(nodes);
  18321. };
  18322. /**
  18323. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  18324. * @param {Number[] | String[]} ids
  18325. * @private
  18326. */
  18327. Network.prototype._removeNodes = function(ids) {
  18328. var nodes = this.nodes;
  18329. for (var i = 0, len = ids.length; i < len; i++) {
  18330. var id = ids[i];
  18331. delete nodes[id];
  18332. }
  18333. this._updateNodeIndexList();
  18334. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18335. this._resetLevels();
  18336. this._setupHierarchicalLayout();
  18337. }
  18338. this._updateCalculationNodes();
  18339. this._reconnectEdges();
  18340. this._updateSelection();
  18341. this._updateValueRange(nodes);
  18342. };
  18343. /**
  18344. * Load edges by reading the data table
  18345. * @param {Array | DataSet | DataView} edges The data containing the edges.
  18346. * @private
  18347. * @private
  18348. */
  18349. Network.prototype._setEdges = function(edges) {
  18350. var oldEdgesData = this.edgesData;
  18351. if (edges instanceof DataSet || edges instanceof DataView) {
  18352. this.edgesData = edges;
  18353. }
  18354. else if (edges instanceof Array) {
  18355. this.edgesData = new DataSet();
  18356. this.edgesData.add(edges);
  18357. }
  18358. else if (!edges) {
  18359. this.edgesData = new DataSet();
  18360. }
  18361. else {
  18362. throw new TypeError('Array or DataSet expected');
  18363. }
  18364. if (oldEdgesData) {
  18365. // unsubscribe from old dataset
  18366. util.forEach(this.edgesListeners, function (callback, event) {
  18367. oldEdgesData.off(event, callback);
  18368. });
  18369. }
  18370. // remove drawn edges
  18371. this.edges = {};
  18372. if (this.edgesData) {
  18373. // subscribe to new dataset
  18374. var me = this;
  18375. util.forEach(this.edgesListeners, function (callback, event) {
  18376. me.edgesData.on(event, callback);
  18377. });
  18378. // draw all new nodes
  18379. var ids = this.edgesData.getIds();
  18380. this._addEdges(ids);
  18381. }
  18382. this._reconnectEdges();
  18383. };
  18384. /**
  18385. * Add edges
  18386. * @param {Number[] | String[]} ids
  18387. * @private
  18388. */
  18389. Network.prototype._addEdges = function (ids) {
  18390. var edges = this.edges,
  18391. edgesData = this.edgesData;
  18392. for (var i = 0, len = ids.length; i < len; i++) {
  18393. var id = ids[i];
  18394. var oldEdge = edges[id];
  18395. if (oldEdge) {
  18396. oldEdge.disconnect();
  18397. }
  18398. var data = edgesData.get(id, {"showInternalIds" : true});
  18399. edges[id] = new Edge(data, this, this.constants);
  18400. }
  18401. this.moving = true;
  18402. this._updateValueRange(edges);
  18403. this._createBezierNodes();
  18404. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18405. this._resetLevels();
  18406. this._setupHierarchicalLayout();
  18407. }
  18408. this._updateCalculationNodes();
  18409. };
  18410. /**
  18411. * Update existing edges, or create them when not yet existing
  18412. * @param {Number[] | String[]} ids
  18413. * @private
  18414. */
  18415. Network.prototype._updateEdges = function (ids) {
  18416. var edges = this.edges,
  18417. edgesData = this.edgesData;
  18418. for (var i = 0, len = ids.length; i < len; i++) {
  18419. var id = ids[i];
  18420. var data = edgesData.get(id);
  18421. var edge = edges[id];
  18422. if (edge) {
  18423. // update edge
  18424. edge.disconnect();
  18425. edge.setProperties(data, this.constants);
  18426. edge.connect();
  18427. }
  18428. else {
  18429. // create edge
  18430. edge = new Edge(data, this, this.constants);
  18431. this.edges[id] = edge;
  18432. }
  18433. }
  18434. this._createBezierNodes();
  18435. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18436. this._resetLevels();
  18437. this._setupHierarchicalLayout();
  18438. }
  18439. this.moving = true;
  18440. this._updateValueRange(edges);
  18441. };
  18442. /**
  18443. * Remove existing edges. Non existing ids will be ignored
  18444. * @param {Number[] | String[]} ids
  18445. * @private
  18446. */
  18447. Network.prototype._removeEdges = function (ids) {
  18448. var edges = this.edges;
  18449. for (var i = 0, len = ids.length; i < len; i++) {
  18450. var id = ids[i];
  18451. var edge = edges[id];
  18452. if (edge) {
  18453. if (edge.via != null) {
  18454. delete this.sectors['support']['nodes'][edge.via.id];
  18455. }
  18456. edge.disconnect();
  18457. delete edges[id];
  18458. }
  18459. }
  18460. this.moving = true;
  18461. this._updateValueRange(edges);
  18462. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  18463. this._resetLevels();
  18464. this._setupHierarchicalLayout();
  18465. }
  18466. this._updateCalculationNodes();
  18467. };
  18468. /**
  18469. * Reconnect all edges
  18470. * @private
  18471. */
  18472. Network.prototype._reconnectEdges = function() {
  18473. var id,
  18474. nodes = this.nodes,
  18475. edges = this.edges;
  18476. for (id in nodes) {
  18477. if (nodes.hasOwnProperty(id)) {
  18478. nodes[id].edges = [];
  18479. }
  18480. }
  18481. for (id in edges) {
  18482. if (edges.hasOwnProperty(id)) {
  18483. var edge = edges[id];
  18484. edge.from = null;
  18485. edge.to = null;
  18486. edge.connect();
  18487. }
  18488. }
  18489. };
  18490. /**
  18491. * Update the values of all object in the given array according to the current
  18492. * value range of the objects in the array.
  18493. * @param {Object} obj An object containing a set of Edges or Nodes
  18494. * The objects must have a method getValue() and
  18495. * setValueRange(min, max).
  18496. * @private
  18497. */
  18498. Network.prototype._updateValueRange = function(obj) {
  18499. var id;
  18500. // determine the range of the objects
  18501. var valueMin = undefined;
  18502. var valueMax = undefined;
  18503. for (id in obj) {
  18504. if (obj.hasOwnProperty(id)) {
  18505. var value = obj[id].getValue();
  18506. if (value !== undefined) {
  18507. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  18508. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  18509. }
  18510. }
  18511. }
  18512. // adjust the range of all objects
  18513. if (valueMin !== undefined && valueMax !== undefined) {
  18514. for (id in obj) {
  18515. if (obj.hasOwnProperty(id)) {
  18516. obj[id].setValueRange(valueMin, valueMax);
  18517. }
  18518. }
  18519. }
  18520. };
  18521. /**
  18522. * Redraw the network with the current data
  18523. * chart will be resized too.
  18524. */
  18525. Network.prototype.redraw = function() {
  18526. this.setSize(this.width, this.height);
  18527. this._redraw();
  18528. };
  18529. /**
  18530. * Redraw the network with the current data
  18531. * @private
  18532. */
  18533. Network.prototype._redraw = function() {
  18534. var ctx = this.frame.canvas.getContext('2d');
  18535. // clear the canvas
  18536. var w = this.frame.canvas.width;
  18537. var h = this.frame.canvas.height;
  18538. ctx.clearRect(0, 0, w, h);
  18539. // set scaling and translation
  18540. ctx.save();
  18541. ctx.translate(this.translation.x, this.translation.y);
  18542. ctx.scale(this.scale, this.scale);
  18543. this.canvasTopLeft = {
  18544. "x": this._XconvertDOMtoCanvas(0),
  18545. "y": this._YconvertDOMtoCanvas(0)
  18546. };
  18547. this.canvasBottomRight = {
  18548. "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
  18549. "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
  18550. };
  18551. this._doInAllSectors("_drawAllSectorNodes",ctx);
  18552. this._doInAllSectors("_drawEdges",ctx);
  18553. this._doInAllSectors("_drawNodes",ctx,false);
  18554. this._doInAllSectors("_drawControlNodes",ctx);
  18555. // this._doInSupportSector("_drawNodes",ctx,true);
  18556. // this._drawTree(ctx,"#F00F0F");
  18557. // restore original scaling and translation
  18558. ctx.restore();
  18559. };
  18560. /**
  18561. * Set the translation of the network
  18562. * @param {Number} offsetX Horizontal offset
  18563. * @param {Number} offsetY Vertical offset
  18564. * @private
  18565. */
  18566. Network.prototype._setTranslation = function(offsetX, offsetY) {
  18567. if (this.translation === undefined) {
  18568. this.translation = {
  18569. x: 0,
  18570. y: 0
  18571. };
  18572. }
  18573. if (offsetX !== undefined) {
  18574. this.translation.x = offsetX;
  18575. }
  18576. if (offsetY !== undefined) {
  18577. this.translation.y = offsetY;
  18578. }
  18579. this.emit('viewChanged');
  18580. };
  18581. /**
  18582. * Get the translation of the network
  18583. * @return {Object} translation An object with parameters x and y, both a number
  18584. * @private
  18585. */
  18586. Network.prototype._getTranslation = function() {
  18587. return {
  18588. x: this.translation.x,
  18589. y: this.translation.y
  18590. };
  18591. };
  18592. /**
  18593. * Scale the network
  18594. * @param {Number} scale Scaling factor 1.0 is unscaled
  18595. * @private
  18596. */
  18597. Network.prototype._setScale = function(scale) {
  18598. this.scale = scale;
  18599. };
  18600. /**
  18601. * Get the current scale of the network
  18602. * @return {Number} scale Scaling factor 1.0 is unscaled
  18603. * @private
  18604. */
  18605. Network.prototype._getScale = function() {
  18606. return this.scale;
  18607. };
  18608. /**
  18609. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  18610. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  18611. * @param {number} x
  18612. * @returns {number}
  18613. * @private
  18614. */
  18615. Network.prototype._XconvertDOMtoCanvas = function(x) {
  18616. return (x - this.translation.x) / this.scale;
  18617. };
  18618. /**
  18619. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  18620. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  18621. * @param {number} x
  18622. * @returns {number}
  18623. * @private
  18624. */
  18625. Network.prototype._XconvertCanvasToDOM = function(x) {
  18626. return x * this.scale + this.translation.x;
  18627. };
  18628. /**
  18629. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  18630. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  18631. * @param {number} y
  18632. * @returns {number}
  18633. * @private
  18634. */
  18635. Network.prototype._YconvertDOMtoCanvas = function(y) {
  18636. return (y - this.translation.y) / this.scale;
  18637. };
  18638. /**
  18639. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  18640. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  18641. * @param {number} y
  18642. * @returns {number}
  18643. * @private
  18644. */
  18645. Network.prototype._YconvertCanvasToDOM = function(y) {
  18646. return y * this.scale + this.translation.y ;
  18647. };
  18648. /**
  18649. *
  18650. * @param {object} pos = {x: number, y: number}
  18651. * @returns {{x: number, y: number}}
  18652. * @constructor
  18653. */
  18654. Network.prototype.canvasToDOM = function(pos) {
  18655. return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
  18656. }
  18657. /**
  18658. *
  18659. * @param {object} pos = {x: number, y: number}
  18660. * @returns {{x: number, y: number}}
  18661. * @constructor
  18662. */
  18663. Network.prototype.DOMtoCanvas = function(pos) {
  18664. return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
  18665. }
  18666. /**
  18667. * Redraw all nodes
  18668. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  18669. * @param {CanvasRenderingContext2D} ctx
  18670. * @param {Boolean} [alwaysShow]
  18671. * @private
  18672. */
  18673. Network.prototype._drawNodes = function(ctx,alwaysShow) {
  18674. if (alwaysShow === undefined) {
  18675. alwaysShow = false;
  18676. }
  18677. // first draw the unselected nodes
  18678. var nodes = this.nodes;
  18679. var selected = [];
  18680. for (var id in nodes) {
  18681. if (nodes.hasOwnProperty(id)) {
  18682. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  18683. if (nodes[id].isSelected()) {
  18684. selected.push(id);
  18685. }
  18686. else {
  18687. if (nodes[id].inArea() || alwaysShow) {
  18688. nodes[id].draw(ctx);
  18689. }
  18690. }
  18691. }
  18692. }
  18693. // draw the selected nodes on top
  18694. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  18695. if (nodes[selected[s]].inArea() || alwaysShow) {
  18696. nodes[selected[s]].draw(ctx);
  18697. }
  18698. }
  18699. };
  18700. /**
  18701. * Redraw all edges
  18702. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  18703. * @param {CanvasRenderingContext2D} ctx
  18704. * @private
  18705. */
  18706. Network.prototype._drawEdges = function(ctx) {
  18707. var edges = this.edges;
  18708. for (var id in edges) {
  18709. if (edges.hasOwnProperty(id)) {
  18710. var edge = edges[id];
  18711. edge.setScale(this.scale);
  18712. if (edge.connected) {
  18713. edges[id].draw(ctx);
  18714. }
  18715. }
  18716. }
  18717. };
  18718. /**
  18719. * Redraw all edges
  18720. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  18721. * @param {CanvasRenderingContext2D} ctx
  18722. * @private
  18723. */
  18724. Network.prototype._drawControlNodes = function(ctx) {
  18725. var edges = this.edges;
  18726. for (var id in edges) {
  18727. if (edges.hasOwnProperty(id)) {
  18728. edges[id]._drawControlNodes(ctx);
  18729. }
  18730. }
  18731. };
  18732. /**
  18733. * Find a stable position for all nodes
  18734. * @private
  18735. */
  18736. Network.prototype._stabilize = function() {
  18737. if (this.constants.freezeForStabilization == true) {
  18738. this._freezeDefinedNodes();
  18739. }
  18740. // find stable position
  18741. var count = 0;
  18742. while (this.moving && count < this.constants.stabilizationIterations) {
  18743. this._physicsTick();
  18744. count++;
  18745. }
  18746. this.zoomExtent(false,true);
  18747. if (this.constants.freezeForStabilization == true) {
  18748. this._restoreFrozenNodes();
  18749. }
  18750. this.emit("stabilized",{iterations:count});
  18751. };
  18752. /**
  18753. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  18754. * because only the supportnodes for the smoothCurves have to settle.
  18755. *
  18756. * @private
  18757. */
  18758. Network.prototype._freezeDefinedNodes = function() {
  18759. var nodes = this.nodes;
  18760. for (var id in nodes) {
  18761. if (nodes.hasOwnProperty(id)) {
  18762. if (nodes[id].x != null && nodes[id].y != null) {
  18763. nodes[id].fixedData.x = nodes[id].xFixed;
  18764. nodes[id].fixedData.y = nodes[id].yFixed;
  18765. nodes[id].xFixed = true;
  18766. nodes[id].yFixed = true;
  18767. }
  18768. }
  18769. }
  18770. };
  18771. /**
  18772. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  18773. *
  18774. * @private
  18775. */
  18776. Network.prototype._restoreFrozenNodes = function() {
  18777. var nodes = this.nodes;
  18778. for (var id in nodes) {
  18779. if (nodes.hasOwnProperty(id)) {
  18780. if (nodes[id].fixedData.x != null) {
  18781. nodes[id].xFixed = nodes[id].fixedData.x;
  18782. nodes[id].yFixed = nodes[id].fixedData.y;
  18783. }
  18784. }
  18785. }
  18786. };
  18787. /**
  18788. * Check if any of the nodes is still moving
  18789. * @param {number} vmin the minimum velocity considered as 'moving'
  18790. * @return {boolean} true if moving, false if non of the nodes is moving
  18791. * @private
  18792. */
  18793. Network.prototype._isMoving = function(vmin) {
  18794. var nodes = this.nodes;
  18795. for (var id in nodes) {
  18796. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  18797. return true;
  18798. }
  18799. }
  18800. return false;
  18801. };
  18802. /**
  18803. * /**
  18804. * Perform one discrete step for all nodes
  18805. *
  18806. * @private
  18807. */
  18808. Network.prototype._discreteStepNodes = function() {
  18809. var interval = this.physicsDiscreteStepsize;
  18810. var nodes = this.nodes;
  18811. var nodeId;
  18812. var nodesPresent = false;
  18813. if (this.constants.maxVelocity > 0) {
  18814. for (nodeId in nodes) {
  18815. if (nodes.hasOwnProperty(nodeId)) {
  18816. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  18817. nodesPresent = true;
  18818. }
  18819. }
  18820. }
  18821. else {
  18822. for (nodeId in nodes) {
  18823. if (nodes.hasOwnProperty(nodeId)) {
  18824. nodes[nodeId].discreteStep(interval);
  18825. nodesPresent = true;
  18826. }
  18827. }
  18828. }
  18829. if (nodesPresent == true) {
  18830. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  18831. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  18832. this.moving = true;
  18833. }
  18834. else {
  18835. this.moving = this._isMoving(vminCorrected);
  18836. }
  18837. }
  18838. };
  18839. /**
  18840. * A single simulation step (or "tick") in the physics simulation
  18841. *
  18842. * @private
  18843. */
  18844. Network.prototype._physicsTick = function() {
  18845. if (!this.freezeSimulation) {
  18846. if (this.moving) {
  18847. this._doInAllActiveSectors("_initializeForceCalculation");
  18848. this._doInAllActiveSectors("_discreteStepNodes");
  18849. if (this.constants.smoothCurves) {
  18850. this._doInSupportSector("_discreteStepNodes");
  18851. }
  18852. this._findCenter(this._getRange())
  18853. }
  18854. }
  18855. };
  18856. /**
  18857. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  18858. * It reschedules itself at the beginning of the function
  18859. *
  18860. * @private
  18861. */
  18862. Network.prototype._animationStep = function() {
  18863. // reset the timer so a new scheduled animation step can be set
  18864. this.timer = undefined;
  18865. // handle the keyboad movement
  18866. this._handleNavigation();
  18867. // this schedules a new animation step
  18868. this.start();
  18869. // start the physics simulation
  18870. var calculationTime = Date.now();
  18871. var maxSteps = 1;
  18872. this._physicsTick();
  18873. var timeRequired = Date.now() - calculationTime;
  18874. while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  18875. this._physicsTick();
  18876. timeRequired = Date.now() - calculationTime;
  18877. maxSteps++;
  18878. }
  18879. // start the rendering process
  18880. var renderTime = Date.now();
  18881. this._redraw();
  18882. this.renderTime = Date.now() - renderTime;
  18883. };
  18884. if (typeof window !== 'undefined') {
  18885. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  18886. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  18887. }
  18888. /**
  18889. * Schedule a animation step with the refreshrate interval.
  18890. */
  18891. Network.prototype.start = function() {
  18892. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  18893. if (!this.timer) {
  18894. var ua = navigator.userAgent.toLowerCase();
  18895. var requiresTimeout = false;
  18896. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  18897. requiresTimeout = true;
  18898. }
  18899. else if (ua.indexOf('safari') != -1) { // safari
  18900. if (ua.indexOf('chrome') <= -1) {
  18901. requiresTimeout = true;
  18902. }
  18903. }
  18904. if (requiresTimeout == true) {
  18905. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  18906. }
  18907. else{
  18908. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  18909. }
  18910. }
  18911. }
  18912. else {
  18913. this._redraw();
  18914. }
  18915. };
  18916. /**
  18917. * Move the network according to the keyboard presses.
  18918. *
  18919. * @private
  18920. */
  18921. Network.prototype._handleNavigation = function() {
  18922. if (this.xIncrement != 0 || this.yIncrement != 0) {
  18923. var translation = this._getTranslation();
  18924. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  18925. }
  18926. if (this.zoomIncrement != 0) {
  18927. var center = {
  18928. x: this.frame.canvas.clientWidth / 2,
  18929. y: this.frame.canvas.clientHeight / 2
  18930. };
  18931. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  18932. }
  18933. };
  18934. /**
  18935. * Freeze the _animationStep
  18936. */
  18937. Network.prototype.toggleFreeze = function() {
  18938. if (this.freezeSimulation == false) {
  18939. this.freezeSimulation = true;
  18940. }
  18941. else {
  18942. this.freezeSimulation = false;
  18943. this.start();
  18944. }
  18945. };
  18946. /**
  18947. * This function cleans the support nodes if they are not needed and adds them when they are.
  18948. *
  18949. * @param {boolean} [disableStart]
  18950. * @private
  18951. */
  18952. Network.prototype._configureSmoothCurves = function(disableStart) {
  18953. if (disableStart === undefined) {
  18954. disableStart = true;
  18955. }
  18956. if (this.constants.smoothCurves == true) {
  18957. this._createBezierNodes();
  18958. }
  18959. else {
  18960. // delete the support nodes
  18961. this.sectors['support']['nodes'] = {};
  18962. for (var edgeId in this.edges) {
  18963. if (this.edges.hasOwnProperty(edgeId)) {
  18964. this.edges[edgeId].smooth = false;
  18965. this.edges[edgeId].via = null;
  18966. }
  18967. }
  18968. }
  18969. this._updateCalculationNodes();
  18970. if (!disableStart) {
  18971. this.moving = true;
  18972. this.start();
  18973. }
  18974. };
  18975. /**
  18976. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  18977. * are used for the force calculation.
  18978. *
  18979. * @private
  18980. */
  18981. Network.prototype._createBezierNodes = function() {
  18982. if (this.constants.smoothCurves == true) {
  18983. for (var edgeId in this.edges) {
  18984. if (this.edges.hasOwnProperty(edgeId)) {
  18985. var edge = this.edges[edgeId];
  18986. if (edge.via == null) {
  18987. edge.smooth = true;
  18988. var nodeId = "edgeId:".concat(edge.id);
  18989. this.sectors['support']['nodes'][nodeId] = new Node(
  18990. {id:nodeId,
  18991. mass:1,
  18992. shape:'circle',
  18993. image:"",
  18994. internalMultiplier:1
  18995. },{},{},this.constants);
  18996. edge.via = this.sectors['support']['nodes'][nodeId];
  18997. edge.via.parentEdgeId = edge.id;
  18998. edge.positionBezierNode();
  18999. }
  19000. }
  19001. }
  19002. }
  19003. };
  19004. /**
  19005. * load the functions that load the mixins into the prototype.
  19006. *
  19007. * @private
  19008. */
  19009. Network.prototype._initializeMixinLoaders = function () {
  19010. for (var mixinFunction in networkMixinLoaders) {
  19011. if (networkMixinLoaders.hasOwnProperty(mixinFunction)) {
  19012. Network.prototype[mixinFunction] = networkMixinLoaders[mixinFunction];
  19013. }
  19014. }
  19015. };
  19016. /**
  19017. * Load the XY positions of the nodes into the dataset.
  19018. */
  19019. Network.prototype.storePosition = function() {
  19020. var dataArray = [];
  19021. for (var nodeId in this.nodes) {
  19022. if (this.nodes.hasOwnProperty(nodeId)) {
  19023. var node = this.nodes[nodeId];
  19024. var allowedToMoveX = !this.nodes.xFixed;
  19025. var allowedToMoveY = !this.nodes.yFixed;
  19026. if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) {
  19027. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  19028. }
  19029. }
  19030. }
  19031. this.nodesData.update(dataArray);
  19032. };
  19033. /**
  19034. * Center a node in view.
  19035. *
  19036. * @param {Number} nodeId
  19037. * @param {Number} [zoomLevel]
  19038. */
  19039. Network.prototype.focusOnNode = function (nodeId, zoomLevel) {
  19040. if (this.nodes.hasOwnProperty(nodeId)) {
  19041. if (zoomLevel === undefined) {
  19042. zoomLevel = this._getScale();
  19043. }
  19044. var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
  19045. var requiredScale = zoomLevel;
  19046. this._setScale(requiredScale);
  19047. var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height});
  19048. var translation = this._getTranslation();
  19049. var distanceFromCenter = {x:canvasCenter.x - nodePosition.x,
  19050. y:canvasCenter.y - nodePosition.y};
  19051. this._setTranslation(translation.x + requiredScale * distanceFromCenter.x,
  19052. translation.y + requiredScale * distanceFromCenter.y);
  19053. this.redraw();
  19054. }
  19055. else {
  19056. console.log("This nodeId cannot be found.")
  19057. }
  19058. };
  19059. /**
  19060. * @constructor Graph3d
  19061. * Graph3d displays data in 3d.
  19062. *
  19063. * Graph3d is developed in javascript as a Google Visualization Chart.
  19064. *
  19065. * @param {Element} container The DOM element in which the Graph3d will
  19066. * be created. Normally a div element.
  19067. * @param {DataSet | DataView | Array} [data]
  19068. * @param {Object} [options]
  19069. */
  19070. function Graph3d(container, data, options) {
  19071. if (!(this instanceof Graph3d)) {
  19072. throw new SyntaxError('Constructor must be called with the new operator');
  19073. }
  19074. // create variables and set default values
  19075. this.containerElement = container;
  19076. this.width = '400px';
  19077. this.height = '400px';
  19078. this.margin = 10; // px
  19079. this.defaultXCenter = '55%';
  19080. this.defaultYCenter = '50%';
  19081. this.xLabel = 'x';
  19082. this.yLabel = 'y';
  19083. this.zLabel = 'z';
  19084. this.filterLabel = 'time';
  19085. this.legendLabel = 'value';
  19086. this.style = Graph3d.STYLE.DOT;
  19087. this.showPerspective = true;
  19088. this.showGrid = true;
  19089. this.keepAspectRatio = true;
  19090. this.showShadow = false;
  19091. this.showGrayBottom = false; // TODO: this does not work correctly
  19092. this.showTooltip = false;
  19093. this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
  19094. this.animationInterval = 1000; // milliseconds
  19095. this.animationPreload = false;
  19096. this.camera = new Graph3d.Camera();
  19097. this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
  19098. this.dataTable = null; // The original data table
  19099. this.dataPoints = null; // The table with point objects
  19100. // the column indexes
  19101. this.colX = undefined;
  19102. this.colY = undefined;
  19103. this.colZ = undefined;
  19104. this.colValue = undefined;
  19105. this.colFilter = undefined;
  19106. this.xMin = 0;
  19107. this.xStep = undefined; // auto by default
  19108. this.xMax = 1;
  19109. this.yMin = 0;
  19110. this.yStep = undefined; // auto by default
  19111. this.yMax = 1;
  19112. this.zMin = 0;
  19113. this.zStep = undefined; // auto by default
  19114. this.zMax = 1;
  19115. this.valueMin = 0;
  19116. this.valueMax = 1;
  19117. this.xBarWidth = 1;
  19118. this.yBarWidth = 1;
  19119. // TODO: customize axis range
  19120. // constants
  19121. this.colorAxis = '#4D4D4D';
  19122. this.colorGrid = '#D3D3D3';
  19123. this.colorDot = '#7DC1FF';
  19124. this.colorDotBorder = '#3267D2';
  19125. // create a frame and canvas
  19126. this.create();
  19127. // apply options (also when undefined)
  19128. this.setOptions(options);
  19129. // apply data
  19130. if (data) {
  19131. this.setData(data);
  19132. }
  19133. }
  19134. // Extend Graph3d with an Emitter mixin
  19135. Emitter(Graph3d.prototype);
  19136. /**
  19137. * @class Camera
  19138. * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
  19139. * The camera is always looking in the direction of the origin of the arm.
  19140. * This way, the camera always rotates around one fixed point, the location
  19141. * of the camera arm.
  19142. *
  19143. * Documentation:
  19144. * http://en.wikipedia.org/wiki/3D_projection
  19145. */
  19146. Graph3d.Camera = function () {
  19147. this.armLocation = new Point3d();
  19148. this.armRotation = {};
  19149. this.armRotation.horizontal = 0;
  19150. this.armRotation.vertical = 0;
  19151. this.armLength = 1.7;
  19152. this.cameraLocation = new Point3d();
  19153. this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
  19154. this.calculateCameraOrientation();
  19155. };
  19156. /**
  19157. * Set the location (origin) of the arm
  19158. * @param {Number} x Normalized value of x
  19159. * @param {Number} y Normalized value of y
  19160. * @param {Number} z Normalized value of z
  19161. */
  19162. Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
  19163. this.armLocation.x = x;
  19164. this.armLocation.y = y;
  19165. this.armLocation.z = z;
  19166. this.calculateCameraOrientation();
  19167. };
  19168. /**
  19169. * Set the rotation of the camera arm
  19170. * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
  19171. * Optional, can be left undefined.
  19172. * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
  19173. * if vertical=0.5*PI, the graph is shown from the
  19174. * top. Optional, can be left undefined.
  19175. */
  19176. Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
  19177. if (horizontal !== undefined) {
  19178. this.armRotation.horizontal = horizontal;
  19179. }
  19180. if (vertical !== undefined) {
  19181. this.armRotation.vertical = vertical;
  19182. if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
  19183. if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
  19184. }
  19185. if (horizontal !== undefined || vertical !== undefined) {
  19186. this.calculateCameraOrientation();
  19187. }
  19188. };
  19189. /**
  19190. * Retrieve the current arm rotation
  19191. * @return {object} An object with parameters horizontal and vertical
  19192. */
  19193. Graph3d.Camera.prototype.getArmRotation = function() {
  19194. var rot = {};
  19195. rot.horizontal = this.armRotation.horizontal;
  19196. rot.vertical = this.armRotation.vertical;
  19197. return rot;
  19198. };
  19199. /**
  19200. * Set the (normalized) length of the camera arm.
  19201. * @param {Number} length A length between 0.71 and 5.0
  19202. */
  19203. Graph3d.Camera.prototype.setArmLength = function(length) {
  19204. if (length === undefined)
  19205. return;
  19206. this.armLength = length;
  19207. // Radius must be larger than the corner of the graph,
  19208. // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
  19209. // graph
  19210. if (this.armLength < 0.71) this.armLength = 0.71;
  19211. if (this.armLength > 5.0) this.armLength = 5.0;
  19212. this.calculateCameraOrientation();
  19213. };
  19214. /**
  19215. * Retrieve the arm length
  19216. * @return {Number} length
  19217. */
  19218. Graph3d.Camera.prototype.getArmLength = function() {
  19219. return this.armLength;
  19220. };
  19221. /**
  19222. * Retrieve the camera location
  19223. * @return {Point3d} cameraLocation
  19224. */
  19225. Graph3d.Camera.prototype.getCameraLocation = function() {
  19226. return this.cameraLocation;
  19227. };
  19228. /**
  19229. * Retrieve the camera rotation
  19230. * @return {Point3d} cameraRotation
  19231. */
  19232. Graph3d.Camera.prototype.getCameraRotation = function() {
  19233. return this.cameraRotation;
  19234. };
  19235. /**
  19236. * Calculate the location and rotation of the camera based on the
  19237. * position and orientation of the camera arm
  19238. */
  19239. Graph3d.Camera.prototype.calculateCameraOrientation = function() {
  19240. // calculate location of the camera
  19241. this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  19242. this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  19243. this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
  19244. // calculate rotation of the camera
  19245. this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
  19246. this.cameraRotation.y = 0;
  19247. this.cameraRotation.z = -this.armRotation.horizontal;
  19248. };
  19249. /**
  19250. * Calculate the scaling values, dependent on the range in x, y, and z direction
  19251. */
  19252. Graph3d.prototype._setScale = function() {
  19253. this.scale = new Point3d(1 / (this.xMax - this.xMin),
  19254. 1 / (this.yMax - this.yMin),
  19255. 1 / (this.zMax - this.zMin));
  19256. // keep aspect ration between x and y scale if desired
  19257. if (this.keepAspectRatio) {
  19258. if (this.scale.x < this.scale.y) {
  19259. //noinspection JSSuspiciousNameCombination
  19260. this.scale.y = this.scale.x;
  19261. }
  19262. else {
  19263. //noinspection JSSuspiciousNameCombination
  19264. this.scale.x = this.scale.y;
  19265. }
  19266. }
  19267. // scale the vertical axis
  19268. this.scale.z *= this.verticalRatio;
  19269. // TODO: can this be automated? verticalRatio?
  19270. // determine scale for (optional) value
  19271. this.scale.value = 1 / (this.valueMax - this.valueMin);
  19272. // position the camera arm
  19273. var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
  19274. var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
  19275. var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
  19276. this.camera.setArmLocation(xCenter, yCenter, zCenter);
  19277. };
  19278. /**
  19279. * Convert a 3D location to a 2D location on screen
  19280. * http://en.wikipedia.org/wiki/3D_projection
  19281. * @param {Point3d} point3d A 3D point with parameters x, y, z
  19282. * @return {Point2d} point2d A 2D point with parameters x, y
  19283. */
  19284. Graph3d.prototype._convert3Dto2D = function(point3d) {
  19285. var translation = this._convertPointToTranslation(point3d);
  19286. return this._convertTranslationToScreen(translation);
  19287. };
  19288. /**
  19289. * Convert a 3D location its translation seen from the camera
  19290. * http://en.wikipedia.org/wiki/3D_projection
  19291. * @param {Point3d} point3d A 3D point with parameters x, y, z
  19292. * @return {Point3d} translation A 3D point with parameters x, y, z This is
  19293. * the translation of the point, seen from the
  19294. * camera
  19295. */
  19296. Graph3d.prototype._convertPointToTranslation = function(point3d) {
  19297. var ax = point3d.x * this.scale.x,
  19298. ay = point3d.y * this.scale.y,
  19299. az = point3d.z * this.scale.z,
  19300. cx = this.camera.getCameraLocation().x,
  19301. cy = this.camera.getCameraLocation().y,
  19302. cz = this.camera.getCameraLocation().z,
  19303. // calculate angles
  19304. sinTx = Math.sin(this.camera.getCameraRotation().x),
  19305. cosTx = Math.cos(this.camera.getCameraRotation().x),
  19306. sinTy = Math.sin(this.camera.getCameraRotation().y),
  19307. cosTy = Math.cos(this.camera.getCameraRotation().y),
  19308. sinTz = Math.sin(this.camera.getCameraRotation().z),
  19309. cosTz = Math.cos(this.camera.getCameraRotation().z),
  19310. // calculate translation
  19311. dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
  19312. dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
  19313. dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
  19314. return new Point3d(dx, dy, dz);
  19315. };
  19316. /**
  19317. * Convert a translation point to a point on the screen
  19318. * @param {Point3d} translation A 3D point with parameters x, y, z This is
  19319. * the translation of the point, seen from the
  19320. * camera
  19321. * @return {Point2d} point2d A 2D point with parameters x, y
  19322. */
  19323. Graph3d.prototype._convertTranslationToScreen = function(translation) {
  19324. var ex = this.eye.x,
  19325. ey = this.eye.y,
  19326. ez = this.eye.z,
  19327. dx = translation.x,
  19328. dy = translation.y,
  19329. dz = translation.z;
  19330. // calculate position on screen from translation
  19331. var bx;
  19332. var by;
  19333. if (this.showPerspective) {
  19334. bx = (dx - ex) * (ez / dz);
  19335. by = (dy - ey) * (ez / dz);
  19336. }
  19337. else {
  19338. bx = dx * -(ez / this.camera.getArmLength());
  19339. by = dy * -(ez / this.camera.getArmLength());
  19340. }
  19341. // shift and scale the point to the center of the screen
  19342. // use the width of the graph to scale both horizontally and vertically.
  19343. return new Point2d(
  19344. this.xcenter + bx * this.frame.canvas.clientWidth,
  19345. this.ycenter - by * this.frame.canvas.clientWidth);
  19346. };
  19347. /**
  19348. * Set the background styling for the graph
  19349. * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
  19350. */
  19351. Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
  19352. var fill = 'white';
  19353. var stroke = 'gray';
  19354. var strokeWidth = 1;
  19355. if (typeof(backgroundColor) === 'string') {
  19356. fill = backgroundColor;
  19357. stroke = 'none';
  19358. strokeWidth = 0;
  19359. }
  19360. else if (typeof(backgroundColor) === 'object') {
  19361. if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
  19362. if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
  19363. if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
  19364. }
  19365. else if (backgroundColor === undefined) {
  19366. // use use defaults
  19367. }
  19368. else {
  19369. throw 'Unsupported type of backgroundColor';
  19370. }
  19371. this.frame.style.backgroundColor = fill;
  19372. this.frame.style.borderColor = stroke;
  19373. this.frame.style.borderWidth = strokeWidth + 'px';
  19374. this.frame.style.borderStyle = 'solid';
  19375. };
  19376. /// enumerate the available styles
  19377. Graph3d.STYLE = {
  19378. BAR: 0,
  19379. BARCOLOR: 1,
  19380. BARSIZE: 2,
  19381. DOT : 3,
  19382. DOTLINE : 4,
  19383. DOTCOLOR: 5,
  19384. DOTSIZE: 6,
  19385. GRID : 7,
  19386. LINE: 8,
  19387. SURFACE : 9
  19388. };
  19389. /**
  19390. * Retrieve the style index from given styleName
  19391. * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
  19392. * @return {Number} styleNumber Enumeration value representing the style, or -1
  19393. * when not found
  19394. */
  19395. Graph3d.prototype._getStyleNumber = function(styleName) {
  19396. switch (styleName) {
  19397. case 'dot': return Graph3d.STYLE.DOT;
  19398. case 'dot-line': return Graph3d.STYLE.DOTLINE;
  19399. case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
  19400. case 'dot-size': return Graph3d.STYLE.DOTSIZE;
  19401. case 'line': return Graph3d.STYLE.LINE;
  19402. case 'grid': return Graph3d.STYLE.GRID;
  19403. case 'surface': return Graph3d.STYLE.SURFACE;
  19404. case 'bar': return Graph3d.STYLE.BAR;
  19405. case 'bar-color': return Graph3d.STYLE.BARCOLOR;
  19406. case 'bar-size': return Graph3d.STYLE.BARSIZE;
  19407. }
  19408. return -1;
  19409. };
  19410. /**
  19411. * Determine the indexes of the data columns, based on the given style and data
  19412. * @param {DataSet} data
  19413. * @param {Number} style
  19414. */
  19415. Graph3d.prototype._determineColumnIndexes = function(data, style) {
  19416. if (this.style === Graph3d.STYLE.DOT ||
  19417. this.style === Graph3d.STYLE.DOTLINE ||
  19418. this.style === Graph3d.STYLE.LINE ||
  19419. this.style === Graph3d.STYLE.GRID ||
  19420. this.style === Graph3d.STYLE.SURFACE ||
  19421. this.style === Graph3d.STYLE.BAR) {
  19422. // 3 columns expected, and optionally a 4th with filter values
  19423. this.colX = 0;
  19424. this.colY = 1;
  19425. this.colZ = 2;
  19426. this.colValue = undefined;
  19427. if (data.getNumberOfColumns() > 3) {
  19428. this.colFilter = 3;
  19429. }
  19430. }
  19431. else if (this.style === Graph3d.STYLE.DOTCOLOR ||
  19432. this.style === Graph3d.STYLE.DOTSIZE ||
  19433. this.style === Graph3d.STYLE.BARCOLOR ||
  19434. this.style === Graph3d.STYLE.BARSIZE) {
  19435. // 4 columns expected, and optionally a 5th with filter values
  19436. this.colX = 0;
  19437. this.colY = 1;
  19438. this.colZ = 2;
  19439. this.colValue = 3;
  19440. if (data.getNumberOfColumns() > 4) {
  19441. this.colFilter = 4;
  19442. }
  19443. }
  19444. else {
  19445. throw 'Unknown style "' + this.style + '"';
  19446. }
  19447. };
  19448. Graph3d.prototype.getNumberOfRows = function(data) {
  19449. return data.length;
  19450. }
  19451. Graph3d.prototype.getNumberOfColumns = function(data) {
  19452. var counter = 0;
  19453. for (var column in data[0]) {
  19454. if (data[0].hasOwnProperty(column)) {
  19455. counter++;
  19456. }
  19457. }
  19458. return counter;
  19459. }
  19460. Graph3d.prototype.getDistinctValues = function(data, column) {
  19461. var distinctValues = [];
  19462. for (var i = 0; i < data.length; i++) {
  19463. if (distinctValues.indexOf(data[i][column]) == -1) {
  19464. distinctValues.push(data[i][column]);
  19465. }
  19466. }
  19467. return distinctValues;
  19468. }
  19469. Graph3d.prototype.getColumnRange = function(data,column) {
  19470. var minMax = {min:data[0][column],max:data[0][column]};
  19471. for (var i = 0; i < data.length; i++) {
  19472. if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
  19473. if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
  19474. }
  19475. return minMax;
  19476. };
  19477. /**
  19478. * Initialize the data from the data table. Calculate minimum and maximum values
  19479. * and column index values
  19480. * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
  19481. * @param {Number} style Style Number
  19482. */
  19483. Graph3d.prototype._dataInitialize = function (rawData, style) {
  19484. var me = this;
  19485. // unsubscribe from the dataTable
  19486. if (this.dataSet) {
  19487. this.dataSet.off('*', this._onChange);
  19488. }
  19489. if (rawData === undefined)
  19490. return;
  19491. if (Array.isArray(rawData)) {
  19492. rawData = new DataSet(rawData);
  19493. }
  19494. var data;
  19495. if (rawData instanceof DataSet || rawData instanceof DataView) {
  19496. data = rawData.get();
  19497. }
  19498. else {
  19499. throw new Error('Array, DataSet, or DataView expected');
  19500. }
  19501. if (data.length == 0)
  19502. return;
  19503. this.dataSet = rawData;
  19504. this.dataTable = data;
  19505. // subscribe to changes in the dataset
  19506. this._onChange = function () {
  19507. me.setData(me.dataSet);
  19508. };
  19509. this.dataSet.on('*', this._onChange);
  19510. // _determineColumnIndexes
  19511. // getNumberOfRows (points)
  19512. // getNumberOfColumns (x,y,z,v,t,t1,t2...)
  19513. // getDistinctValues (unique values?)
  19514. // getColumnRange
  19515. // determine the location of x,y,z,value,filter columns
  19516. this.colX = 'x';
  19517. this.colY = 'y';
  19518. this.colZ = 'z';
  19519. this.colValue = 'style';
  19520. this.colFilter = 'filter';
  19521. // check if a filter column is provided
  19522. if (data[0].hasOwnProperty('filter')) {
  19523. if (this.dataFilter === undefined) {
  19524. this.dataFilter = new Filter(rawData, this.colFilter, this);
  19525. this.dataFilter.setOnLoadCallback(function() {me.redraw();});
  19526. }
  19527. }
  19528. var withBars = this.style == Graph3d.STYLE.BAR ||
  19529. this.style == Graph3d.STYLE.BARCOLOR ||
  19530. this.style == Graph3d.STYLE.BARSIZE;
  19531. // determine barWidth from data
  19532. if (withBars) {
  19533. if (this.defaultXBarWidth !== undefined) {
  19534. this.xBarWidth = this.defaultXBarWidth;
  19535. }
  19536. else {
  19537. var dataX = this.getDistinctValues(data,this.colX);
  19538. this.xBarWidth = (dataX[1] - dataX[0]) || 1;
  19539. }
  19540. if (this.defaultYBarWidth !== undefined) {
  19541. this.yBarWidth = this.defaultYBarWidth;
  19542. }
  19543. else {
  19544. var dataY = this.getDistinctValues(data,this.colY);
  19545. this.yBarWidth = (dataY[1] - dataY[0]) || 1;
  19546. }
  19547. }
  19548. // calculate minimums and maximums
  19549. var xRange = this.getColumnRange(data,this.colX);
  19550. if (withBars) {
  19551. xRange.min -= this.xBarWidth / 2;
  19552. xRange.max += this.xBarWidth / 2;
  19553. }
  19554. this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
  19555. this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
  19556. if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
  19557. this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
  19558. var yRange = this.getColumnRange(data,this.colY);
  19559. if (withBars) {
  19560. yRange.min -= this.yBarWidth / 2;
  19561. yRange.max += this.yBarWidth / 2;
  19562. }
  19563. this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
  19564. this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
  19565. if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
  19566. this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
  19567. var zRange = this.getColumnRange(data,this.colZ);
  19568. this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
  19569. this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
  19570. if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
  19571. this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
  19572. if (this.colValue !== undefined) {
  19573. var valueRange = this.getColumnRange(data,this.colValue);
  19574. this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
  19575. this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
  19576. if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
  19577. }
  19578. // set the scale dependent on the ranges.
  19579. this._setScale();
  19580. };
  19581. /**
  19582. * Filter the data based on the current filter
  19583. * @param {Array} data
  19584. * @return {Array} dataPoints Array with point objects which can be drawn on screen
  19585. */
  19586. Graph3d.prototype._getDataPoints = function (data) {
  19587. // TODO: store the created matrix dataPoints in the filters instead of reloading each time
  19588. var x, y, i, z, obj, point;
  19589. var dataPoints = [];
  19590. if (this.style === Graph3d.STYLE.GRID ||
  19591. this.style === Graph3d.STYLE.SURFACE) {
  19592. // copy all values from the google data table to a matrix
  19593. // the provided values are supposed to form a grid of (x,y) positions
  19594. // create two lists with all present x and y values
  19595. var dataX = [];
  19596. var dataY = [];
  19597. for (i = 0; i < this.getNumberOfRows(data); i++) {
  19598. x = data[i][this.colX] || 0;
  19599. y = data[i][this.colY] || 0;
  19600. if (dataX.indexOf(x) === -1) {
  19601. dataX.push(x);
  19602. }
  19603. if (dataY.indexOf(y) === -1) {
  19604. dataY.push(y);
  19605. }
  19606. }
  19607. function sortNumber(a, b) {
  19608. return a - b;
  19609. }
  19610. dataX.sort(sortNumber);
  19611. dataY.sort(sortNumber);
  19612. // create a grid, a 2d matrix, with all values.
  19613. var dataMatrix = []; // temporary data matrix
  19614. for (i = 0; i < data.length; i++) {
  19615. x = data[i][this.colX] || 0;
  19616. y = data[i][this.colY] || 0;
  19617. z = data[i][this.colZ] || 0;
  19618. var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
  19619. var yIndex = dataY.indexOf(y);
  19620. if (dataMatrix[xIndex] === undefined) {
  19621. dataMatrix[xIndex] = [];
  19622. }
  19623. var point3d = new Point3d();
  19624. point3d.x = x;
  19625. point3d.y = y;
  19626. point3d.z = z;
  19627. obj = {};
  19628. obj.point = point3d;
  19629. obj.trans = undefined;
  19630. obj.screen = undefined;
  19631. obj.bottom = new Point3d(x, y, this.zMin);
  19632. dataMatrix[xIndex][yIndex] = obj;
  19633. dataPoints.push(obj);
  19634. }
  19635. // fill in the pointers to the neighbors.
  19636. for (x = 0; x < dataMatrix.length; x++) {
  19637. for (y = 0; y < dataMatrix[x].length; y++) {
  19638. if (dataMatrix[x][y]) {
  19639. dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
  19640. dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
  19641. dataMatrix[x][y].pointCross =
  19642. (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
  19643. dataMatrix[x+1][y+1] :
  19644. undefined;
  19645. }
  19646. }
  19647. }
  19648. }
  19649. else { // 'dot', 'dot-line', etc.
  19650. // copy all values from the google data table to a list with Point3d objects
  19651. for (i = 0; i < data.length; i++) {
  19652. point = new Point3d();
  19653. point.x = data[i][this.colX] || 0;
  19654. point.y = data[i][this.colY] || 0;
  19655. point.z = data[i][this.colZ] || 0;
  19656. if (this.colValue !== undefined) {
  19657. point.value = data[i][this.colValue] || 0;
  19658. }
  19659. obj = {};
  19660. obj.point = point;
  19661. obj.bottom = new Point3d(point.x, point.y, this.zMin);
  19662. obj.trans = undefined;
  19663. obj.screen = undefined;
  19664. dataPoints.push(obj);
  19665. }
  19666. }
  19667. return dataPoints;
  19668. };
  19669. /**
  19670. * Append suffix 'px' to provided value x
  19671. * @param {int} x An integer value
  19672. * @return {string} the string value of x, followed by the suffix 'px'
  19673. */
  19674. Graph3d.px = function(x) {
  19675. return x + 'px';
  19676. };
  19677. /**
  19678. * Create the main frame for the Graph3d.
  19679. * This function is executed once when a Graph3d object is created. The frame
  19680. * contains a canvas, and this canvas contains all objects like the axis and
  19681. * nodes.
  19682. */
  19683. Graph3d.prototype.create = function () {
  19684. // remove all elements from the container element.
  19685. while (this.containerElement.hasChildNodes()) {
  19686. this.containerElement.removeChild(this.containerElement.firstChild);
  19687. }
  19688. this.frame = document.createElement('div');
  19689. this.frame.style.position = 'relative';
  19690. this.frame.style.overflow = 'hidden';
  19691. // create the graph canvas (HTML canvas element)
  19692. this.frame.canvas = document.createElement( 'canvas' );
  19693. this.frame.canvas.style.position = 'relative';
  19694. this.frame.appendChild(this.frame.canvas);
  19695. //if (!this.frame.canvas.getContext) {
  19696. {
  19697. var noCanvas = document.createElement( 'DIV' );
  19698. noCanvas.style.color = 'red';
  19699. noCanvas.style.fontWeight = 'bold' ;
  19700. noCanvas.style.padding = '10px';
  19701. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  19702. this.frame.canvas.appendChild(noCanvas);
  19703. }
  19704. this.frame.filter = document.createElement( 'div' );
  19705. this.frame.filter.style.position = 'absolute';
  19706. this.frame.filter.style.bottom = '0px';
  19707. this.frame.filter.style.left = '0px';
  19708. this.frame.filter.style.width = '100%';
  19709. this.frame.appendChild(this.frame.filter);
  19710. // add event listeners to handle moving and zooming the contents
  19711. var me = this;
  19712. var onmousedown = function (event) {me._onMouseDown(event);};
  19713. var ontouchstart = function (event) {me._onTouchStart(event);};
  19714. var onmousewheel = function (event) {me._onWheel(event);};
  19715. var ontooltip = function (event) {me._onTooltip(event);};
  19716. // TODO: these events are never cleaned up... can give a 'memory leakage'
  19717. G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
  19718. G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
  19719. G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
  19720. G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
  19721. G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
  19722. // add the new graph to the container element
  19723. this.containerElement.appendChild(this.frame);
  19724. };
  19725. /**
  19726. * Set a new size for the graph
  19727. * @param {string} width Width in pixels or percentage (for example '800px'
  19728. * or '50%')
  19729. * @param {string} height Height in pixels or percentage (for example '400px'
  19730. * or '30%')
  19731. */
  19732. Graph3d.prototype.setSize = function(width, height) {
  19733. this.frame.style.width = width;
  19734. this.frame.style.height = height;
  19735. this._resizeCanvas();
  19736. };
  19737. /**
  19738. * Resize the canvas to the current size of the frame
  19739. */
  19740. Graph3d.prototype._resizeCanvas = function() {
  19741. this.frame.canvas.style.width = '100%';
  19742. this.frame.canvas.style.height = '100%';
  19743. this.frame.canvas.width = this.frame.canvas.clientWidth;
  19744. this.frame.canvas.height = this.frame.canvas.clientHeight;
  19745. // adjust with for margin
  19746. this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
  19747. };
  19748. /**
  19749. * Start animation
  19750. */
  19751. Graph3d.prototype.animationStart = function() {
  19752. if (!this.frame.filter || !this.frame.filter.slider)
  19753. throw 'No animation available';
  19754. this.frame.filter.slider.play();
  19755. };
  19756. /**
  19757. * Stop animation
  19758. */
  19759. Graph3d.prototype.animationStop = function() {
  19760. if (!this.frame.filter || !this.frame.filter.slider) return;
  19761. this.frame.filter.slider.stop();
  19762. };
  19763. /**
  19764. * Resize the center position based on the current values in this.defaultXCenter
  19765. * and this.defaultYCenter (which are strings with a percentage or a value
  19766. * in pixels). The center positions are the variables this.xCenter
  19767. * and this.yCenter
  19768. */
  19769. Graph3d.prototype._resizeCenter = function() {
  19770. // calculate the horizontal center position
  19771. if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
  19772. this.xcenter =
  19773. parseFloat(this.defaultXCenter) / 100 *
  19774. this.frame.canvas.clientWidth;
  19775. }
  19776. else {
  19777. this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
  19778. }
  19779. // calculate the vertical center position
  19780. if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
  19781. this.ycenter =
  19782. parseFloat(this.defaultYCenter) / 100 *
  19783. (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
  19784. }
  19785. else {
  19786. this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
  19787. }
  19788. };
  19789. /**
  19790. * Set the rotation and distance of the camera
  19791. * @param {Object} pos An object with the camera position. The object
  19792. * contains three parameters:
  19793. * - horizontal {Number}
  19794. * The horizontal rotation, between 0 and 2*PI.
  19795. * Optional, can be left undefined.
  19796. * - vertical {Number}
  19797. * The vertical rotation, between 0 and 0.5*PI
  19798. * if vertical=0.5*PI, the graph is shown from the
  19799. * top. Optional, can be left undefined.
  19800. * - distance {Number}
  19801. * The (normalized) distance of the camera to the
  19802. * center of the graph, a value between 0.71 and 5.0.
  19803. * Optional, can be left undefined.
  19804. */
  19805. Graph3d.prototype.setCameraPosition = function(pos) {
  19806. if (pos === undefined) {
  19807. return;
  19808. }
  19809. if (pos.horizontal !== undefined && pos.vertical !== undefined) {
  19810. this.camera.setArmRotation(pos.horizontal, pos.vertical);
  19811. }
  19812. if (pos.distance !== undefined) {
  19813. this.camera.setArmLength(pos.distance);
  19814. }
  19815. this.redraw();
  19816. };
  19817. /**
  19818. * Retrieve the current camera rotation
  19819. * @return {object} An object with parameters horizontal, vertical, and
  19820. * distance
  19821. */
  19822. Graph3d.prototype.getCameraPosition = function() {
  19823. var pos = this.camera.getArmRotation();
  19824. pos.distance = this.camera.getArmLength();
  19825. return pos;
  19826. };
  19827. /**
  19828. * Load data into the 3D Graph
  19829. */
  19830. Graph3d.prototype._readData = function(data) {
  19831. // read the data
  19832. this._dataInitialize(data, this.style);
  19833. if (this.dataFilter) {
  19834. // apply filtering
  19835. this.dataPoints = this.dataFilter._getDataPoints();
  19836. }
  19837. else {
  19838. // no filtering. load all data
  19839. this.dataPoints = this._getDataPoints(this.dataTable);
  19840. }
  19841. // draw the filter
  19842. this._redrawFilter();
  19843. };
  19844. /**
  19845. * Replace the dataset of the Graph3d
  19846. * @param {Array | DataSet | DataView} data
  19847. */
  19848. Graph3d.prototype.setData = function (data) {
  19849. this._readData(data);
  19850. this.redraw();
  19851. // start animation when option is true
  19852. if (this.animationAutoStart && this.dataFilter) {
  19853. this.animationStart();
  19854. }
  19855. };
  19856. /**
  19857. * Update the options. Options will be merged with current options
  19858. * @param {Object} options
  19859. */
  19860. Graph3d.prototype.setOptions = function (options) {
  19861. var cameraPosition = undefined;
  19862. this.animationStop();
  19863. if (options !== undefined) {
  19864. // retrieve parameter values
  19865. if (options.width !== undefined) this.width = options.width;
  19866. if (options.height !== undefined) this.height = options.height;
  19867. if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
  19868. if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
  19869. if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
  19870. if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
  19871. if (options.xLabel !== undefined) this.xLabel = options.xLabel;
  19872. if (options.yLabel !== undefined) this.yLabel = options.yLabel;
  19873. if (options.zLabel !== undefined) this.zLabel = options.zLabel;
  19874. if (options.style !== undefined) {
  19875. var styleNumber = this._getStyleNumber(options.style);
  19876. if (styleNumber !== -1) {
  19877. this.style = styleNumber;
  19878. }
  19879. }
  19880. if (options.showGrid !== undefined) this.showGrid = options.showGrid;
  19881. if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
  19882. if (options.showShadow !== undefined) this.showShadow = options.showShadow;
  19883. if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
  19884. if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
  19885. if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
  19886. if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
  19887. if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
  19888. if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
  19889. if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
  19890. if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
  19891. if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
  19892. if (options.xMin !== undefined) this.defaultXMin = options.xMin;
  19893. if (options.xStep !== undefined) this.defaultXStep = options.xStep;
  19894. if (options.xMax !== undefined) this.defaultXMax = options.xMax;
  19895. if (options.yMin !== undefined) this.defaultYMin = options.yMin;
  19896. if (options.yStep !== undefined) this.defaultYStep = options.yStep;
  19897. if (options.yMax !== undefined) this.defaultYMax = options.yMax;
  19898. if (options.zMin !== undefined) this.defaultZMin = options.zMin;
  19899. if (options.zStep !== undefined) this.defaultZStep = options.zStep;
  19900. if (options.zMax !== undefined) this.defaultZMax = options.zMax;
  19901. if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
  19902. if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
  19903. if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
  19904. if (cameraPosition !== undefined) {
  19905. this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
  19906. this.camera.setArmLength(cameraPosition.distance);
  19907. }
  19908. else {
  19909. this.camera.setArmRotation(1.0, 0.5);
  19910. this.camera.setArmLength(1.7);
  19911. }
  19912. }
  19913. this._setBackgroundColor(options && options.backgroundColor);
  19914. this.setSize(this.width, this.height);
  19915. // re-load the data
  19916. if (this.dataTable) {
  19917. this.setData(this.dataTable);
  19918. }
  19919. // start animation when option is true
  19920. if (this.animationAutoStart && this.dataFilter) {
  19921. this.animationStart();
  19922. }
  19923. };
  19924. /**
  19925. * Redraw the Graph.
  19926. */
  19927. Graph3d.prototype.redraw = function() {
  19928. if (this.dataPoints === undefined) {
  19929. throw 'Error: graph data not initialized';
  19930. }
  19931. this._resizeCanvas();
  19932. this._resizeCenter();
  19933. this._redrawSlider();
  19934. this._redrawClear();
  19935. this._redrawAxis();
  19936. if (this.style === Graph3d.STYLE.GRID ||
  19937. this.style === Graph3d.STYLE.SURFACE) {
  19938. this._redrawDataGrid();
  19939. }
  19940. else if (this.style === Graph3d.STYLE.LINE) {
  19941. this._redrawDataLine();
  19942. }
  19943. else if (this.style === Graph3d.STYLE.BAR ||
  19944. this.style === Graph3d.STYLE.BARCOLOR ||
  19945. this.style === Graph3d.STYLE.BARSIZE) {
  19946. this._redrawDataBar();
  19947. }
  19948. else {
  19949. // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
  19950. this._redrawDataDot();
  19951. }
  19952. this._redrawInfo();
  19953. this._redrawLegend();
  19954. };
  19955. /**
  19956. * Clear the canvas before redrawing
  19957. */
  19958. Graph3d.prototype._redrawClear = function() {
  19959. var canvas = this.frame.canvas;
  19960. var ctx = canvas.getContext('2d');
  19961. ctx.clearRect(0, 0, canvas.width, canvas.height);
  19962. };
  19963. /**
  19964. * Redraw the legend showing the colors
  19965. */
  19966. Graph3d.prototype._redrawLegend = function() {
  19967. var y;
  19968. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  19969. this.style === Graph3d.STYLE.DOTSIZE) {
  19970. var dotSize = this.frame.clientWidth * 0.02;
  19971. var widthMin, widthMax;
  19972. if (this.style === Graph3d.STYLE.DOTSIZE) {
  19973. widthMin = dotSize / 2; // px
  19974. widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
  19975. }
  19976. else {
  19977. widthMin = 20; // px
  19978. widthMax = 20; // px
  19979. }
  19980. var height = Math.max(this.frame.clientHeight * 0.25, 100);
  19981. var top = this.margin;
  19982. var right = this.frame.clientWidth - this.margin;
  19983. var left = right - widthMax;
  19984. var bottom = top + height;
  19985. }
  19986. var canvas = this.frame.canvas;
  19987. var ctx = canvas.getContext('2d');
  19988. ctx.lineWidth = 1;
  19989. ctx.font = '14px arial'; // TODO: put in options
  19990. if (this.style === Graph3d.STYLE.DOTCOLOR) {
  19991. // draw the color bar
  19992. var ymin = 0;
  19993. var ymax = height; // Todo: make height customizable
  19994. for (y = ymin; y < ymax; y++) {
  19995. var f = (y - ymin) / (ymax - ymin);
  19996. //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
  19997. var hue = f * 240;
  19998. var color = this._hsv2rgb(hue, 1, 1);
  19999. ctx.strokeStyle = color;
  20000. ctx.beginPath();
  20001. ctx.moveTo(left, top + y);
  20002. ctx.lineTo(right, top + y);
  20003. ctx.stroke();
  20004. }
  20005. ctx.strokeStyle = this.colorAxis;
  20006. ctx.strokeRect(left, top, widthMax, height);
  20007. }
  20008. if (this.style === Graph3d.STYLE.DOTSIZE) {
  20009. // draw border around color bar
  20010. ctx.strokeStyle = this.colorAxis;
  20011. ctx.fillStyle = this.colorDot;
  20012. ctx.beginPath();
  20013. ctx.moveTo(left, top);
  20014. ctx.lineTo(right, top);
  20015. ctx.lineTo(right - widthMax + widthMin, bottom);
  20016. ctx.lineTo(left, bottom);
  20017. ctx.closePath();
  20018. ctx.fill();
  20019. ctx.stroke();
  20020. }
  20021. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  20022. this.style === Graph3d.STYLE.DOTSIZE) {
  20023. // print values along the color bar
  20024. var gridLineLen = 5; // px
  20025. var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
  20026. step.start();
  20027. if (step.getCurrent() < this.valueMin) {
  20028. step.next();
  20029. }
  20030. while (!step.end()) {
  20031. y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
  20032. ctx.beginPath();
  20033. ctx.moveTo(left - gridLineLen, y);
  20034. ctx.lineTo(left, y);
  20035. ctx.stroke();
  20036. ctx.textAlign = 'right';
  20037. ctx.textBaseline = 'middle';
  20038. ctx.fillStyle = this.colorAxis;
  20039. ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
  20040. step.next();
  20041. }
  20042. ctx.textAlign = 'right';
  20043. ctx.textBaseline = 'top';
  20044. var label = this.legendLabel;
  20045. ctx.fillText(label, right, bottom + this.margin);
  20046. }
  20047. };
  20048. /**
  20049. * Redraw the filter
  20050. */
  20051. Graph3d.prototype._redrawFilter = function() {
  20052. this.frame.filter.innerHTML = '';
  20053. if (this.dataFilter) {
  20054. var options = {
  20055. 'visible': this.showAnimationControls
  20056. };
  20057. var slider = new Slider(this.frame.filter, options);
  20058. this.frame.filter.slider = slider;
  20059. // TODO: css here is not nice here...
  20060. this.frame.filter.style.padding = '10px';
  20061. //this.frame.filter.style.backgroundColor = '#EFEFEF';
  20062. slider.setValues(this.dataFilter.values);
  20063. slider.setPlayInterval(this.animationInterval);
  20064. // create an event handler
  20065. var me = this;
  20066. var onchange = function () {
  20067. var index = slider.getIndex();
  20068. me.dataFilter.selectValue(index);
  20069. me.dataPoints = me.dataFilter._getDataPoints();
  20070. me.redraw();
  20071. };
  20072. slider.setOnChangeCallback(onchange);
  20073. }
  20074. else {
  20075. this.frame.filter.slider = undefined;
  20076. }
  20077. };
  20078. /**
  20079. * Redraw the slider
  20080. */
  20081. Graph3d.prototype._redrawSlider = function() {
  20082. if ( this.frame.filter.slider !== undefined) {
  20083. this.frame.filter.slider.redraw();
  20084. }
  20085. };
  20086. /**
  20087. * Redraw common information
  20088. */
  20089. Graph3d.prototype._redrawInfo = function() {
  20090. if (this.dataFilter) {
  20091. var canvas = this.frame.canvas;
  20092. var ctx = canvas.getContext('2d');
  20093. ctx.font = '14px arial'; // TODO: put in options
  20094. ctx.lineStyle = 'gray';
  20095. ctx.fillStyle = 'gray';
  20096. ctx.textAlign = 'left';
  20097. ctx.textBaseline = 'top';
  20098. var x = this.margin;
  20099. var y = this.margin;
  20100. ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
  20101. }
  20102. };
  20103. /**
  20104. * Redraw the axis
  20105. */
  20106. Graph3d.prototype._redrawAxis = function() {
  20107. var canvas = this.frame.canvas,
  20108. ctx = canvas.getContext('2d'),
  20109. from, to, step, prettyStep,
  20110. text, xText, yText, zText,
  20111. offset, xOffset, yOffset,
  20112. xMin2d, xMax2d;
  20113. // TODO: get the actual rendered style of the containerElement
  20114. //ctx.font = this.containerElement.style.font;
  20115. ctx.font = 24 / this.camera.getArmLength() + 'px arial';
  20116. // calculate the length for the short grid lines
  20117. var gridLenX = 0.025 / this.scale.x;
  20118. var gridLenY = 0.025 / this.scale.y;
  20119. var textMargin = 5 / this.camera.getArmLength(); // px
  20120. var armAngle = this.camera.getArmRotation().horizontal;
  20121. // draw x-grid lines
  20122. ctx.lineWidth = 1;
  20123. prettyStep = (this.defaultXStep === undefined);
  20124. step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
  20125. step.start();
  20126. if (step.getCurrent() < this.xMin) {
  20127. step.next();
  20128. }
  20129. while (!step.end()) {
  20130. var x = step.getCurrent();
  20131. if (this.showGrid) {
  20132. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  20133. to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  20134. ctx.strokeStyle = this.colorGrid;
  20135. ctx.beginPath();
  20136. ctx.moveTo(from.x, from.y);
  20137. ctx.lineTo(to.x, to.y);
  20138. ctx.stroke();
  20139. }
  20140. else {
  20141. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  20142. to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
  20143. ctx.strokeStyle = this.colorAxis;
  20144. ctx.beginPath();
  20145. ctx.moveTo(from.x, from.y);
  20146. ctx.lineTo(to.x, to.y);
  20147. ctx.stroke();
  20148. from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  20149. to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
  20150. ctx.strokeStyle = this.colorAxis;
  20151. ctx.beginPath();
  20152. ctx.moveTo(from.x, from.y);
  20153. ctx.lineTo(to.x, to.y);
  20154. ctx.stroke();
  20155. }
  20156. yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
  20157. text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
  20158. if (Math.cos(armAngle * 2) > 0) {
  20159. ctx.textAlign = 'center';
  20160. ctx.textBaseline = 'top';
  20161. text.y += textMargin;
  20162. }
  20163. else if (Math.sin(armAngle * 2) < 0){
  20164. ctx.textAlign = 'right';
  20165. ctx.textBaseline = 'middle';
  20166. }
  20167. else {
  20168. ctx.textAlign = 'left';
  20169. ctx.textBaseline = 'middle';
  20170. }
  20171. ctx.fillStyle = this.colorAxis;
  20172. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  20173. step.next();
  20174. }
  20175. // draw y-grid lines
  20176. ctx.lineWidth = 1;
  20177. prettyStep = (this.defaultYStep === undefined);
  20178. step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
  20179. step.start();
  20180. if (step.getCurrent() < this.yMin) {
  20181. step.next();
  20182. }
  20183. while (!step.end()) {
  20184. if (this.showGrid) {
  20185. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  20186. to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  20187. ctx.strokeStyle = this.colorGrid;
  20188. ctx.beginPath();
  20189. ctx.moveTo(from.x, from.y);
  20190. ctx.lineTo(to.x, to.y);
  20191. ctx.stroke();
  20192. }
  20193. else {
  20194. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  20195. to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
  20196. ctx.strokeStyle = this.colorAxis;
  20197. ctx.beginPath();
  20198. ctx.moveTo(from.x, from.y);
  20199. ctx.lineTo(to.x, to.y);
  20200. ctx.stroke();
  20201. from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  20202. to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
  20203. ctx.strokeStyle = this.colorAxis;
  20204. ctx.beginPath();
  20205. ctx.moveTo(from.x, from.y);
  20206. ctx.lineTo(to.x, to.y);
  20207. ctx.stroke();
  20208. }
  20209. xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
  20210. text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
  20211. if (Math.cos(armAngle * 2) < 0) {
  20212. ctx.textAlign = 'center';
  20213. ctx.textBaseline = 'top';
  20214. text.y += textMargin;
  20215. }
  20216. else if (Math.sin(armAngle * 2) > 0){
  20217. ctx.textAlign = 'right';
  20218. ctx.textBaseline = 'middle';
  20219. }
  20220. else {
  20221. ctx.textAlign = 'left';
  20222. ctx.textBaseline = 'middle';
  20223. }
  20224. ctx.fillStyle = this.colorAxis;
  20225. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  20226. step.next();
  20227. }
  20228. // draw z-grid lines and axis
  20229. ctx.lineWidth = 1;
  20230. prettyStep = (this.defaultZStep === undefined);
  20231. step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
  20232. step.start();
  20233. if (step.getCurrent() < this.zMin) {
  20234. step.next();
  20235. }
  20236. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  20237. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  20238. while (!step.end()) {
  20239. // TODO: make z-grid lines really 3d?
  20240. from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
  20241. ctx.strokeStyle = this.colorAxis;
  20242. ctx.beginPath();
  20243. ctx.moveTo(from.x, from.y);
  20244. ctx.lineTo(from.x - textMargin, from.y);
  20245. ctx.stroke();
  20246. ctx.textAlign = 'right';
  20247. ctx.textBaseline = 'middle';
  20248. ctx.fillStyle = this.colorAxis;
  20249. ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
  20250. step.next();
  20251. }
  20252. ctx.lineWidth = 1;
  20253. from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  20254. to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
  20255. ctx.strokeStyle = this.colorAxis;
  20256. ctx.beginPath();
  20257. ctx.moveTo(from.x, from.y);
  20258. ctx.lineTo(to.x, to.y);
  20259. ctx.stroke();
  20260. // draw x-axis
  20261. ctx.lineWidth = 1;
  20262. // line at yMin
  20263. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  20264. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  20265. ctx.strokeStyle = this.colorAxis;
  20266. ctx.beginPath();
  20267. ctx.moveTo(xMin2d.x, xMin2d.y);
  20268. ctx.lineTo(xMax2d.x, xMax2d.y);
  20269. ctx.stroke();
  20270. // line at ymax
  20271. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  20272. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  20273. ctx.strokeStyle = this.colorAxis;
  20274. ctx.beginPath();
  20275. ctx.moveTo(xMin2d.x, xMin2d.y);
  20276. ctx.lineTo(xMax2d.x, xMax2d.y);
  20277. ctx.stroke();
  20278. // draw y-axis
  20279. ctx.lineWidth = 1;
  20280. // line at xMin
  20281. from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  20282. to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  20283. ctx.strokeStyle = this.colorAxis;
  20284. ctx.beginPath();
  20285. ctx.moveTo(from.x, from.y);
  20286. ctx.lineTo(to.x, to.y);
  20287. ctx.stroke();
  20288. // line at xMax
  20289. from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  20290. to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  20291. ctx.strokeStyle = this.colorAxis;
  20292. ctx.beginPath();
  20293. ctx.moveTo(from.x, from.y);
  20294. ctx.lineTo(to.x, to.y);
  20295. ctx.stroke();
  20296. // draw x-label
  20297. var xLabel = this.xLabel;
  20298. if (xLabel.length > 0) {
  20299. yOffset = 0.1 / this.scale.y;
  20300. xText = (this.xMin + this.xMax) / 2;
  20301. yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
  20302. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  20303. if (Math.cos(armAngle * 2) > 0) {
  20304. ctx.textAlign = 'center';
  20305. ctx.textBaseline = 'top';
  20306. }
  20307. else if (Math.sin(armAngle * 2) < 0){
  20308. ctx.textAlign = 'right';
  20309. ctx.textBaseline = 'middle';
  20310. }
  20311. else {
  20312. ctx.textAlign = 'left';
  20313. ctx.textBaseline = 'middle';
  20314. }
  20315. ctx.fillStyle = this.colorAxis;
  20316. ctx.fillText(xLabel, text.x, text.y);
  20317. }
  20318. // draw y-label
  20319. var yLabel = this.yLabel;
  20320. if (yLabel.length > 0) {
  20321. xOffset = 0.1 / this.scale.x;
  20322. xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
  20323. yText = (this.yMin + this.yMax) / 2;
  20324. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  20325. if (Math.cos(armAngle * 2) < 0) {
  20326. ctx.textAlign = 'center';
  20327. ctx.textBaseline = 'top';
  20328. }
  20329. else if (Math.sin(armAngle * 2) > 0){
  20330. ctx.textAlign = 'right';
  20331. ctx.textBaseline = 'middle';
  20332. }
  20333. else {
  20334. ctx.textAlign = 'left';
  20335. ctx.textBaseline = 'middle';
  20336. }
  20337. ctx.fillStyle = this.colorAxis;
  20338. ctx.fillText(yLabel, text.x, text.y);
  20339. }
  20340. // draw z-label
  20341. var zLabel = this.zLabel;
  20342. if (zLabel.length > 0) {
  20343. offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
  20344. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  20345. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  20346. zText = (this.zMin + this.zMax) / 2;
  20347. text = this._convert3Dto2D(new Point3d(xText, yText, zText));
  20348. ctx.textAlign = 'right';
  20349. ctx.textBaseline = 'middle';
  20350. ctx.fillStyle = this.colorAxis;
  20351. ctx.fillText(zLabel, text.x - offset, text.y);
  20352. }
  20353. };
  20354. /**
  20355. * Calculate the color based on the given value.
  20356. * @param {Number} H Hue, a value be between 0 and 360
  20357. * @param {Number} S Saturation, a value between 0 and 1
  20358. * @param {Number} V Value, a value between 0 and 1
  20359. */
  20360. Graph3d.prototype._hsv2rgb = function(H, S, V) {
  20361. var R, G, B, C, Hi, X;
  20362. C = V * S;
  20363. Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
  20364. X = C * (1 - Math.abs(((H/60) % 2) - 1));
  20365. switch (Hi) {
  20366. case 0: R = C; G = X; B = 0; break;
  20367. case 1: R = X; G = C; B = 0; break;
  20368. case 2: R = 0; G = C; B = X; break;
  20369. case 3: R = 0; G = X; B = C; break;
  20370. case 4: R = X; G = 0; B = C; break;
  20371. case 5: R = C; G = 0; B = X; break;
  20372. default: R = 0; G = 0; B = 0; break;
  20373. }
  20374. return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
  20375. };
  20376. /**
  20377. * Draw all datapoints as a grid
  20378. * This function can be used when the style is 'grid'
  20379. */
  20380. Graph3d.prototype._redrawDataGrid = function() {
  20381. var canvas = this.frame.canvas,
  20382. ctx = canvas.getContext('2d'),
  20383. point, right, top, cross,
  20384. i,
  20385. topSideVisible, fillStyle, strokeStyle, lineWidth,
  20386. h, s, v, zAvg;
  20387. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20388. return; // TODO: throw exception?
  20389. // calculate the translations and screen position of all points
  20390. for (i = 0; i < this.dataPoints.length; i++) {
  20391. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20392. var screen = this._convertTranslationToScreen(trans);
  20393. this.dataPoints[i].trans = trans;
  20394. this.dataPoints[i].screen = screen;
  20395. // calculate the translation of the point at the bottom (needed for sorting)
  20396. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  20397. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  20398. }
  20399. // sort the points on depth of their (x,y) position (not on z)
  20400. var sortDepth = function (a, b) {
  20401. return b.dist - a.dist;
  20402. };
  20403. this.dataPoints.sort(sortDepth);
  20404. if (this.style === Graph3d.STYLE.SURFACE) {
  20405. for (i = 0; i < this.dataPoints.length; i++) {
  20406. point = this.dataPoints[i];
  20407. right = this.dataPoints[i].pointRight;
  20408. top = this.dataPoints[i].pointTop;
  20409. cross = this.dataPoints[i].pointCross;
  20410. if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
  20411. if (this.showGrayBottom || this.showShadow) {
  20412. // calculate the cross product of the two vectors from center
  20413. // to left and right, in order to know whether we are looking at the
  20414. // bottom or at the top side. We can also use the cross product
  20415. // for calculating light intensity
  20416. var aDiff = Point3d.subtract(cross.trans, point.trans);
  20417. var bDiff = Point3d.subtract(top.trans, right.trans);
  20418. var crossproduct = Point3d.crossProduct(aDiff, bDiff);
  20419. var len = crossproduct.length();
  20420. // FIXME: there is a bug with determining the surface side (shadow or colored)
  20421. topSideVisible = (crossproduct.z > 0);
  20422. }
  20423. else {
  20424. topSideVisible = true;
  20425. }
  20426. if (topSideVisible) {
  20427. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20428. zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
  20429. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20430. s = 1; // saturation
  20431. if (this.showShadow) {
  20432. v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
  20433. fillStyle = this._hsv2rgb(h, s, v);
  20434. strokeStyle = fillStyle;
  20435. }
  20436. else {
  20437. v = 1;
  20438. fillStyle = this._hsv2rgb(h, s, v);
  20439. strokeStyle = this.colorAxis;
  20440. }
  20441. }
  20442. else {
  20443. fillStyle = 'gray';
  20444. strokeStyle = this.colorAxis;
  20445. }
  20446. lineWidth = 0.5;
  20447. ctx.lineWidth = lineWidth;
  20448. ctx.fillStyle = fillStyle;
  20449. ctx.strokeStyle = strokeStyle;
  20450. ctx.beginPath();
  20451. ctx.moveTo(point.screen.x, point.screen.y);
  20452. ctx.lineTo(right.screen.x, right.screen.y);
  20453. ctx.lineTo(cross.screen.x, cross.screen.y);
  20454. ctx.lineTo(top.screen.x, top.screen.y);
  20455. ctx.closePath();
  20456. ctx.fill();
  20457. ctx.stroke();
  20458. }
  20459. }
  20460. }
  20461. else { // grid style
  20462. for (i = 0; i < this.dataPoints.length; i++) {
  20463. point = this.dataPoints[i];
  20464. right = this.dataPoints[i].pointRight;
  20465. top = this.dataPoints[i].pointTop;
  20466. if (point !== undefined) {
  20467. if (this.showPerspective) {
  20468. lineWidth = 2 / -point.trans.z;
  20469. }
  20470. else {
  20471. lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
  20472. }
  20473. }
  20474. if (point !== undefined && right !== undefined) {
  20475. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20476. zAvg = (point.point.z + right.point.z) / 2;
  20477. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20478. ctx.lineWidth = lineWidth;
  20479. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  20480. ctx.beginPath();
  20481. ctx.moveTo(point.screen.x, point.screen.y);
  20482. ctx.lineTo(right.screen.x, right.screen.y);
  20483. ctx.stroke();
  20484. }
  20485. if (point !== undefined && top !== undefined) {
  20486. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20487. zAvg = (point.point.z + top.point.z) / 2;
  20488. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20489. ctx.lineWidth = lineWidth;
  20490. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  20491. ctx.beginPath();
  20492. ctx.moveTo(point.screen.x, point.screen.y);
  20493. ctx.lineTo(top.screen.x, top.screen.y);
  20494. ctx.stroke();
  20495. }
  20496. }
  20497. }
  20498. };
  20499. /**
  20500. * Draw all datapoints as dots.
  20501. * This function can be used when the style is 'dot' or 'dot-line'
  20502. */
  20503. Graph3d.prototype._redrawDataDot = function() {
  20504. var canvas = this.frame.canvas;
  20505. var ctx = canvas.getContext('2d');
  20506. var i;
  20507. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20508. return; // TODO: throw exception?
  20509. // calculate the translations of all points
  20510. for (i = 0; i < this.dataPoints.length; i++) {
  20511. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20512. var screen = this._convertTranslationToScreen(trans);
  20513. this.dataPoints[i].trans = trans;
  20514. this.dataPoints[i].screen = screen;
  20515. // calculate the distance from the point at the bottom to the camera
  20516. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  20517. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  20518. }
  20519. // order the translated points by depth
  20520. var sortDepth = function (a, b) {
  20521. return b.dist - a.dist;
  20522. };
  20523. this.dataPoints.sort(sortDepth);
  20524. // draw the datapoints as colored circles
  20525. var dotSize = this.frame.clientWidth * 0.02; // px
  20526. for (i = 0; i < this.dataPoints.length; i++) {
  20527. var point = this.dataPoints[i];
  20528. if (this.style === Graph3d.STYLE.DOTLINE) {
  20529. // draw a vertical line from the bottom to the graph value
  20530. //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
  20531. var from = this._convert3Dto2D(point.bottom);
  20532. ctx.lineWidth = 1;
  20533. ctx.strokeStyle = this.colorGrid;
  20534. ctx.beginPath();
  20535. ctx.moveTo(from.x, from.y);
  20536. ctx.lineTo(point.screen.x, point.screen.y);
  20537. ctx.stroke();
  20538. }
  20539. // calculate radius for the circle
  20540. var size;
  20541. if (this.style === Graph3d.STYLE.DOTSIZE) {
  20542. size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
  20543. }
  20544. else {
  20545. size = dotSize;
  20546. }
  20547. var radius;
  20548. if (this.showPerspective) {
  20549. radius = size / -point.trans.z;
  20550. }
  20551. else {
  20552. radius = size * -(this.eye.z / this.camera.getArmLength());
  20553. }
  20554. if (radius < 0) {
  20555. radius = 0;
  20556. }
  20557. var hue, color, borderColor;
  20558. if (this.style === Graph3d.STYLE.DOTCOLOR ) {
  20559. // calculate the color based on the value
  20560. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  20561. color = this._hsv2rgb(hue, 1, 1);
  20562. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20563. }
  20564. else if (this.style === Graph3d.STYLE.DOTSIZE) {
  20565. color = this.colorDot;
  20566. borderColor = this.colorDotBorder;
  20567. }
  20568. else {
  20569. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20570. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20571. color = this._hsv2rgb(hue, 1, 1);
  20572. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20573. }
  20574. // draw the circle
  20575. ctx.lineWidth = 1.0;
  20576. ctx.strokeStyle = borderColor;
  20577. ctx.fillStyle = color;
  20578. ctx.beginPath();
  20579. ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
  20580. ctx.fill();
  20581. ctx.stroke();
  20582. }
  20583. };
  20584. /**
  20585. * Draw all datapoints as bars.
  20586. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
  20587. */
  20588. Graph3d.prototype._redrawDataBar = function() {
  20589. var canvas = this.frame.canvas;
  20590. var ctx = canvas.getContext('2d');
  20591. var i, j, surface, corners;
  20592. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20593. return; // TODO: throw exception?
  20594. // calculate the translations of all points
  20595. for (i = 0; i < this.dataPoints.length; i++) {
  20596. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20597. var screen = this._convertTranslationToScreen(trans);
  20598. this.dataPoints[i].trans = trans;
  20599. this.dataPoints[i].screen = screen;
  20600. // calculate the distance from the point at the bottom to the camera
  20601. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  20602. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  20603. }
  20604. // order the translated points by depth
  20605. var sortDepth = function (a, b) {
  20606. return b.dist - a.dist;
  20607. };
  20608. this.dataPoints.sort(sortDepth);
  20609. // draw the datapoints as bars
  20610. var xWidth = this.xBarWidth / 2;
  20611. var yWidth = this.yBarWidth / 2;
  20612. for (i = 0; i < this.dataPoints.length; i++) {
  20613. var point = this.dataPoints[i];
  20614. // determine color
  20615. var hue, color, borderColor;
  20616. if (this.style === Graph3d.STYLE.BARCOLOR ) {
  20617. // calculate the color based on the value
  20618. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  20619. color = this._hsv2rgb(hue, 1, 1);
  20620. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20621. }
  20622. else if (this.style === Graph3d.STYLE.BARSIZE) {
  20623. color = this.colorDot;
  20624. borderColor = this.colorDotBorder;
  20625. }
  20626. else {
  20627. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  20628. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  20629. color = this._hsv2rgb(hue, 1, 1);
  20630. borderColor = this._hsv2rgb(hue, 1, 0.8);
  20631. }
  20632. // calculate size for the bar
  20633. if (this.style === Graph3d.STYLE.BARSIZE) {
  20634. xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  20635. yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  20636. }
  20637. // calculate all corner points
  20638. var me = this;
  20639. var point3d = point.point;
  20640. var top = [
  20641. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
  20642. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
  20643. {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
  20644. {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
  20645. ];
  20646. var bottom = [
  20647. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
  20648. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
  20649. {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
  20650. {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
  20651. ];
  20652. // calculate screen location of the points
  20653. top.forEach(function (obj) {
  20654. obj.screen = me._convert3Dto2D(obj.point);
  20655. });
  20656. bottom.forEach(function (obj) {
  20657. obj.screen = me._convert3Dto2D(obj.point);
  20658. });
  20659. // create five sides, calculate both corner points and center points
  20660. var surfaces = [
  20661. {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
  20662. {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
  20663. {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
  20664. {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
  20665. {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
  20666. ];
  20667. point.surfaces = surfaces;
  20668. // calculate the distance of each of the surface centers to the camera
  20669. for (j = 0; j < surfaces.length; j++) {
  20670. surface = surfaces[j];
  20671. var transCenter = this._convertPointToTranslation(surface.center);
  20672. surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
  20673. // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
  20674. // but the current solution is fast/simple and works in 99.9% of all cases
  20675. // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
  20676. }
  20677. // order the surfaces by their (translated) depth
  20678. surfaces.sort(function (a, b) {
  20679. var diff = b.dist - a.dist;
  20680. if (diff) return diff;
  20681. // if equal depth, sort the top surface last
  20682. if (a.corners === top) return 1;
  20683. if (b.corners === top) return -1;
  20684. // both are equal
  20685. return 0;
  20686. });
  20687. // draw the ordered surfaces
  20688. ctx.lineWidth = 1;
  20689. ctx.strokeStyle = borderColor;
  20690. ctx.fillStyle = color;
  20691. // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
  20692. for (j = 2; j < surfaces.length; j++) {
  20693. surface = surfaces[j];
  20694. corners = surface.corners;
  20695. ctx.beginPath();
  20696. ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
  20697. ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
  20698. ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
  20699. ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
  20700. ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
  20701. ctx.fill();
  20702. ctx.stroke();
  20703. }
  20704. }
  20705. };
  20706. /**
  20707. * Draw a line through all datapoints.
  20708. * This function can be used when the style is 'line'
  20709. */
  20710. Graph3d.prototype._redrawDataLine = function() {
  20711. var canvas = this.frame.canvas,
  20712. ctx = canvas.getContext('2d'),
  20713. point, i;
  20714. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  20715. return; // TODO: throw exception?
  20716. // calculate the translations of all points
  20717. for (i = 0; i < this.dataPoints.length; i++) {
  20718. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  20719. var screen = this._convertTranslationToScreen(trans);
  20720. this.dataPoints[i].trans = trans;
  20721. this.dataPoints[i].screen = screen;
  20722. }
  20723. // start the line
  20724. if (this.dataPoints.length > 0) {
  20725. point = this.dataPoints[0];
  20726. ctx.lineWidth = 1; // TODO: make customizable
  20727. ctx.strokeStyle = 'blue'; // TODO: make customizable
  20728. ctx.beginPath();
  20729. ctx.moveTo(point.screen.x, point.screen.y);
  20730. }
  20731. // draw the datapoints as colored circles
  20732. for (i = 1; i < this.dataPoints.length; i++) {
  20733. point = this.dataPoints[i];
  20734. ctx.lineTo(point.screen.x, point.screen.y);
  20735. }
  20736. // finish the line
  20737. if (this.dataPoints.length > 0) {
  20738. ctx.stroke();
  20739. }
  20740. };
  20741. /**
  20742. * Start a moving operation inside the provided parent element
  20743. * @param {Event} event The event that occurred (required for
  20744. * retrieving the mouse position)
  20745. */
  20746. Graph3d.prototype._onMouseDown = function(event) {
  20747. event = event || window.event;
  20748. // check if mouse is still down (may be up when focus is lost for example
  20749. // in an iframe)
  20750. if (this.leftButtonDown) {
  20751. this._onMouseUp(event);
  20752. }
  20753. // only react on left mouse button down
  20754. this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  20755. if (!this.leftButtonDown && !this.touchDown) return;
  20756. // get mouse position (different code for IE and all other browsers)
  20757. this.startMouseX = getMouseX(event);
  20758. this.startMouseY = getMouseY(event);
  20759. this.startStart = new Date(this.start);
  20760. this.startEnd = new Date(this.end);
  20761. this.startArmRotation = this.camera.getArmRotation();
  20762. this.frame.style.cursor = 'move';
  20763. // add event listeners to handle moving the contents
  20764. // we store the function onmousemove and onmouseup in the graph, so we can
  20765. // remove the eventlisteners lateron in the function mouseUp()
  20766. var me = this;
  20767. this.onmousemove = function (event) {me._onMouseMove(event);};
  20768. this.onmouseup = function (event) {me._onMouseUp(event);};
  20769. G3DaddEventListener(document, 'mousemove', me.onmousemove);
  20770. G3DaddEventListener(document, 'mouseup', me.onmouseup);
  20771. G3DpreventDefault(event);
  20772. };
  20773. /**
  20774. * Perform moving operating.
  20775. * This function activated from within the funcion Graph.mouseDown().
  20776. * @param {Event} event Well, eehh, the event
  20777. */
  20778. Graph3d.prototype._onMouseMove = function (event) {
  20779. event = event || window.event;
  20780. // calculate change in mouse position
  20781. var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
  20782. var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
  20783. var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
  20784. var verticalNew = this.startArmRotation.vertical + diffY / 200;
  20785. var snapAngle = 4; // degrees
  20786. var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
  20787. // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
  20788. // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
  20789. if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
  20790. horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
  20791. }
  20792. if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
  20793. horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
  20794. }
  20795. // snap vertically to nice angles
  20796. if (Math.abs(Math.sin(verticalNew)) < snapValue) {
  20797. verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
  20798. }
  20799. if (Math.abs(Math.cos(verticalNew)) < snapValue) {
  20800. verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
  20801. }
  20802. this.camera.setArmRotation(horizontalNew, verticalNew);
  20803. this.redraw();
  20804. // fire a cameraPositionChange event
  20805. var parameters = this.getCameraPosition();
  20806. this.emit('cameraPositionChange', parameters);
  20807. G3DpreventDefault(event);
  20808. };
  20809. /**
  20810. * Stop moving operating.
  20811. * This function activated from within the funcion Graph.mouseDown().
  20812. * @param {event} event The event
  20813. */
  20814. Graph3d.prototype._onMouseUp = function (event) {
  20815. this.frame.style.cursor = 'auto';
  20816. this.leftButtonDown = false;
  20817. // remove event listeners here
  20818. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  20819. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  20820. G3DpreventDefault(event);
  20821. };
  20822. /**
  20823. * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
  20824. * @param {Event} event A mouse move event
  20825. */
  20826. Graph3d.prototype._onTooltip = function (event) {
  20827. var delay = 300; // ms
  20828. var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
  20829. var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
  20830. if (!this.showTooltip) {
  20831. return;
  20832. }
  20833. if (this.tooltipTimeout) {
  20834. clearTimeout(this.tooltipTimeout);
  20835. }
  20836. // (delayed) display of a tooltip only if no mouse button is down
  20837. if (this.leftButtonDown) {
  20838. this._hideTooltip();
  20839. return;
  20840. }
  20841. if (this.tooltip && this.tooltip.dataPoint) {
  20842. // tooltip is currently visible
  20843. var dataPoint = this._dataPointFromXY(mouseX, mouseY);
  20844. if (dataPoint !== this.tooltip.dataPoint) {
  20845. // datapoint changed
  20846. if (dataPoint) {
  20847. this._showTooltip(dataPoint);
  20848. }
  20849. else {
  20850. this._hideTooltip();
  20851. }
  20852. }
  20853. }
  20854. else {
  20855. // tooltip is currently not visible
  20856. var me = this;
  20857. this.tooltipTimeout = setTimeout(function () {
  20858. me.tooltipTimeout = null;
  20859. // show a tooltip if we have a data point
  20860. var dataPoint = me._dataPointFromXY(mouseX, mouseY);
  20861. if (dataPoint) {
  20862. me._showTooltip(dataPoint);
  20863. }
  20864. }, delay);
  20865. }
  20866. };
  20867. /**
  20868. * Event handler for touchstart event on mobile devices
  20869. */
  20870. Graph3d.prototype._onTouchStart = function(event) {
  20871. this.touchDown = true;
  20872. var me = this;
  20873. this.ontouchmove = function (event) {me._onTouchMove(event);};
  20874. this.ontouchend = function (event) {me._onTouchEnd(event);};
  20875. G3DaddEventListener(document, 'touchmove', me.ontouchmove);
  20876. G3DaddEventListener(document, 'touchend', me.ontouchend);
  20877. this._onMouseDown(event);
  20878. };
  20879. /**
  20880. * Event handler for touchmove event on mobile devices
  20881. */
  20882. Graph3d.prototype._onTouchMove = function(event) {
  20883. this._onMouseMove(event);
  20884. };
  20885. /**
  20886. * Event handler for touchend event on mobile devices
  20887. */
  20888. Graph3d.prototype._onTouchEnd = function(event) {
  20889. this.touchDown = false;
  20890. G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
  20891. G3DremoveEventListener(document, 'touchend', this.ontouchend);
  20892. this._onMouseUp(event);
  20893. };
  20894. /**
  20895. * Event handler for mouse wheel event, used to zoom the graph
  20896. * Code from http://adomas.org/javascript-mouse-wheel/
  20897. * @param {event} event The event
  20898. */
  20899. Graph3d.prototype._onWheel = function(event) {
  20900. if (!event) /* For IE. */
  20901. event = window.event;
  20902. // retrieve delta
  20903. var delta = 0;
  20904. if (event.wheelDelta) { /* IE/Opera. */
  20905. delta = event.wheelDelta/120;
  20906. } else if (event.detail) { /* Mozilla case. */
  20907. // In Mozilla, sign of delta is different than in IE.
  20908. // Also, delta is multiple of 3.
  20909. delta = -event.detail/3;
  20910. }
  20911. // If delta is nonzero, handle it.
  20912. // Basically, delta is now positive if wheel was scrolled up,
  20913. // and negative, if wheel was scrolled down.
  20914. if (delta) {
  20915. var oldLength = this.camera.getArmLength();
  20916. var newLength = oldLength * (1 - delta / 10);
  20917. this.camera.setArmLength(newLength);
  20918. this.redraw();
  20919. this._hideTooltip();
  20920. }
  20921. // fire a cameraPositionChange event
  20922. var parameters = this.getCameraPosition();
  20923. this.emit('cameraPositionChange', parameters);
  20924. // Prevent default actions caused by mouse wheel.
  20925. // That might be ugly, but we handle scrolls somehow
  20926. // anyway, so don't bother here..
  20927. G3DpreventDefault(event);
  20928. };
  20929. /**
  20930. * Test whether a point lies inside given 2D triangle
  20931. * @param {Point2d} point
  20932. * @param {Point2d[]} triangle
  20933. * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
  20934. * @private
  20935. */
  20936. Graph3d.prototype._insideTriangle = function (point, triangle) {
  20937. var a = triangle[0],
  20938. b = triangle[1],
  20939. c = triangle[2];
  20940. function sign (x) {
  20941. return x > 0 ? 1 : x < 0 ? -1 : 0;
  20942. }
  20943. var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
  20944. var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
  20945. var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
  20946. // each of the three signs must be either equal to each other or zero
  20947. return (as == 0 || bs == 0 || as == bs) &&
  20948. (bs == 0 || cs == 0 || bs == cs) &&
  20949. (as == 0 || cs == 0 || as == cs);
  20950. };
  20951. /**
  20952. * Find a data point close to given screen position (x, y)
  20953. * @param {Number} x
  20954. * @param {Number} y
  20955. * @return {Object | null} The closest data point or null if not close to any data point
  20956. * @private
  20957. */
  20958. Graph3d.prototype._dataPointFromXY = function (x, y) {
  20959. var i,
  20960. distMax = 100, // px
  20961. dataPoint = null,
  20962. closestDataPoint = null,
  20963. closestDist = null,
  20964. center = new Point2d(x, y);
  20965. if (this.style === Graph3d.STYLE.BAR ||
  20966. this.style === Graph3d.STYLE.BARCOLOR ||
  20967. this.style === Graph3d.STYLE.BARSIZE) {
  20968. // the data points are ordered from far away to closest
  20969. for (i = this.dataPoints.length - 1; i >= 0; i--) {
  20970. dataPoint = this.dataPoints[i];
  20971. var surfaces = dataPoint.surfaces;
  20972. if (surfaces) {
  20973. for (var s = surfaces.length - 1; s >= 0; s--) {
  20974. // split each surface in two triangles, and see if the center point is inside one of these
  20975. var surface = surfaces[s];
  20976. var corners = surface.corners;
  20977. var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
  20978. var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
  20979. if (this._insideTriangle(center, triangle1) ||
  20980. this._insideTriangle(center, triangle2)) {
  20981. // return immediately at the first hit
  20982. return dataPoint;
  20983. }
  20984. }
  20985. }
  20986. }
  20987. }
  20988. else {
  20989. // find the closest data point, using distance to the center of the point on 2d screen
  20990. for (i = 0; i < this.dataPoints.length; i++) {
  20991. dataPoint = this.dataPoints[i];
  20992. var point = dataPoint.screen;
  20993. if (point) {
  20994. var distX = Math.abs(x - point.x);
  20995. var distY = Math.abs(y - point.y);
  20996. var dist = Math.sqrt(distX * distX + distY * distY);
  20997. if ((closestDist === null || dist < closestDist) && dist < distMax) {
  20998. closestDist = dist;
  20999. closestDataPoint = dataPoint;
  21000. }
  21001. }
  21002. }
  21003. }
  21004. return closestDataPoint;
  21005. };
  21006. /**
  21007. * Display a tooltip for given data point
  21008. * @param {Object} dataPoint
  21009. * @private
  21010. */
  21011. Graph3d.prototype._showTooltip = function (dataPoint) {
  21012. var content, line, dot;
  21013. if (!this.tooltip) {
  21014. content = document.createElement('div');
  21015. content.style.position = 'absolute';
  21016. content.style.padding = '10px';
  21017. content.style.border = '1px solid #4d4d4d';
  21018. content.style.color = '#1a1a1a';
  21019. content.style.background = 'rgba(255,255,255,0.7)';
  21020. content.style.borderRadius = '2px';
  21021. content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
  21022. line = document.createElement('div');
  21023. line.style.position = 'absolute';
  21024. line.style.height = '40px';
  21025. line.style.width = '0';
  21026. line.style.borderLeft = '1px solid #4d4d4d';
  21027. dot = document.createElement('div');
  21028. dot.style.position = 'absolute';
  21029. dot.style.height = '0';
  21030. dot.style.width = '0';
  21031. dot.style.border = '5px solid #4d4d4d';
  21032. dot.style.borderRadius = '5px';
  21033. this.tooltip = {
  21034. dataPoint: null,
  21035. dom: {
  21036. content: content,
  21037. line: line,
  21038. dot: dot
  21039. }
  21040. };
  21041. }
  21042. else {
  21043. content = this.tooltip.dom.content;
  21044. line = this.tooltip.dom.line;
  21045. dot = this.tooltip.dom.dot;
  21046. }
  21047. this._hideTooltip();
  21048. this.tooltip.dataPoint = dataPoint;
  21049. if (typeof this.showTooltip === 'function') {
  21050. content.innerHTML = this.showTooltip(dataPoint.point);
  21051. }
  21052. else {
  21053. content.innerHTML = '<table>' +
  21054. '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
  21055. '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
  21056. '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
  21057. '</table>';
  21058. }
  21059. content.style.left = '0';
  21060. content.style.top = '0';
  21061. this.frame.appendChild(content);
  21062. this.frame.appendChild(line);
  21063. this.frame.appendChild(dot);
  21064. // calculate sizes
  21065. var contentWidth = content.offsetWidth;
  21066. var contentHeight = content.offsetHeight;
  21067. var lineHeight = line.offsetHeight;
  21068. var dotWidth = dot.offsetWidth;
  21069. var dotHeight = dot.offsetHeight;
  21070. var left = dataPoint.screen.x - contentWidth / 2;
  21071. left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
  21072. line.style.left = dataPoint.screen.x + 'px';
  21073. line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
  21074. content.style.left = left + 'px';
  21075. content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
  21076. dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
  21077. dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
  21078. };
  21079. /**
  21080. * Hide the tooltip when displayed
  21081. * @private
  21082. */
  21083. Graph3d.prototype._hideTooltip = function () {
  21084. if (this.tooltip) {
  21085. this.tooltip.dataPoint = null;
  21086. for (var prop in this.tooltip.dom) {
  21087. if (this.tooltip.dom.hasOwnProperty(prop)) {
  21088. var elem = this.tooltip.dom[prop];
  21089. if (elem && elem.parentNode) {
  21090. elem.parentNode.removeChild(elem);
  21091. }
  21092. }
  21093. }
  21094. }
  21095. };
  21096. /**
  21097. * Add and event listener. Works for all browsers
  21098. * @param {Element} element An html element
  21099. * @param {string} action The action, for example 'click',
  21100. * without the prefix 'on'
  21101. * @param {function} listener The callback function to be executed
  21102. * @param {boolean} useCapture
  21103. */
  21104. G3DaddEventListener = function(element, action, listener, useCapture) {
  21105. if (element.addEventListener) {
  21106. if (useCapture === undefined)
  21107. useCapture = false;
  21108. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  21109. action = 'DOMMouseScroll'; // For Firefox
  21110. }
  21111. element.addEventListener(action, listener, useCapture);
  21112. } else {
  21113. element.attachEvent('on' + action, listener); // IE browsers
  21114. }
  21115. };
  21116. /**
  21117. * Remove an event listener from an element
  21118. * @param {Element} element An html dom element
  21119. * @param {string} action The name of the event, for example 'mousedown'
  21120. * @param {function} listener The listener function
  21121. * @param {boolean} useCapture
  21122. */
  21123. G3DremoveEventListener = function(element, action, listener, useCapture) {
  21124. if (element.removeEventListener) {
  21125. // non-IE browsers
  21126. if (useCapture === undefined)
  21127. useCapture = false;
  21128. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  21129. action = 'DOMMouseScroll'; // For Firefox
  21130. }
  21131. element.removeEventListener(action, listener, useCapture);
  21132. } else {
  21133. // IE browsers
  21134. element.detachEvent('on' + action, listener);
  21135. }
  21136. };
  21137. /**
  21138. * Stop event propagation
  21139. */
  21140. G3DstopPropagation = function(event) {
  21141. if (!event)
  21142. event = window.event;
  21143. if (event.stopPropagation) {
  21144. event.stopPropagation(); // non-IE browsers
  21145. }
  21146. else {
  21147. event.cancelBubble = true; // IE browsers
  21148. }
  21149. };
  21150. /**
  21151. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  21152. */
  21153. G3DpreventDefault = function (event) {
  21154. if (!event)
  21155. event = window.event;
  21156. if (event.preventDefault) {
  21157. event.preventDefault(); // non-IE browsers
  21158. }
  21159. else {
  21160. event.returnValue = false; // IE browsers
  21161. }
  21162. };
  21163. /**
  21164. * @prototype Point3d
  21165. * @param {Number} x
  21166. * @param {Number} y
  21167. * @param {Number} z
  21168. */
  21169. function Point3d(x, y, z) {
  21170. this.x = x !== undefined ? x : 0;
  21171. this.y = y !== undefined ? y : 0;
  21172. this.z = z !== undefined ? z : 0;
  21173. };
  21174. /**
  21175. * Subtract the two provided points, returns a-b
  21176. * @param {Point3d} a
  21177. * @param {Point3d} b
  21178. * @return {Point3d} a-b
  21179. */
  21180. Point3d.subtract = function(a, b) {
  21181. var sub = new Point3d();
  21182. sub.x = a.x - b.x;
  21183. sub.y = a.y - b.y;
  21184. sub.z = a.z - b.z;
  21185. return sub;
  21186. };
  21187. /**
  21188. * Add the two provided points, returns a+b
  21189. * @param {Point3d} a
  21190. * @param {Point3d} b
  21191. * @return {Point3d} a+b
  21192. */
  21193. Point3d.add = function(a, b) {
  21194. var sum = new Point3d();
  21195. sum.x = a.x + b.x;
  21196. sum.y = a.y + b.y;
  21197. sum.z = a.z + b.z;
  21198. return sum;
  21199. };
  21200. /**
  21201. * Calculate the average of two 3d points
  21202. * @param {Point3d} a
  21203. * @param {Point3d} b
  21204. * @return {Point3d} The average, (a+b)/2
  21205. */
  21206. Point3d.avg = function(a, b) {
  21207. return new Point3d(
  21208. (a.x + b.x) / 2,
  21209. (a.y + b.y) / 2,
  21210. (a.z + b.z) / 2
  21211. );
  21212. };
  21213. /**
  21214. * Calculate the cross product of the two provided points, returns axb
  21215. * Documentation: http://en.wikipedia.org/wiki/Cross_product
  21216. * @param {Point3d} a
  21217. * @param {Point3d} b
  21218. * @return {Point3d} cross product axb
  21219. */
  21220. Point3d.crossProduct = function(a, b) {
  21221. var crossproduct = new Point3d();
  21222. crossproduct.x = a.y * b.z - a.z * b.y;
  21223. crossproduct.y = a.z * b.x - a.x * b.z;
  21224. crossproduct.z = a.x * b.y - a.y * b.x;
  21225. return crossproduct;
  21226. };
  21227. /**
  21228. * Rtrieve the length of the vector (or the distance from this point to the origin
  21229. * @return {Number} length
  21230. */
  21231. Point3d.prototype.length = function() {
  21232. return Math.sqrt(
  21233. this.x * this.x +
  21234. this.y * this.y +
  21235. this.z * this.z
  21236. );
  21237. };
  21238. /**
  21239. * @prototype Point2d
  21240. */
  21241. Point2d = function (x, y) {
  21242. this.x = x !== undefined ? x : 0;
  21243. this.y = y !== undefined ? y : 0;
  21244. };
  21245. /**
  21246. * @class Filter
  21247. *
  21248. * @param {DataSet} data The google data table
  21249. * @param {Number} column The index of the column to be filtered
  21250. * @param {Graph} graph The graph
  21251. */
  21252. function Filter (data, column, graph) {
  21253. this.data = data;
  21254. this.column = column;
  21255. this.graph = graph; // the parent graph
  21256. this.index = undefined;
  21257. this.value = undefined;
  21258. // read all distinct values and select the first one
  21259. this.values = graph.getDistinctValues(data.get(), this.column);
  21260. // sort both numeric and string values correctly
  21261. this.values.sort(function (a, b) {
  21262. return a > b ? 1 : a < b ? -1 : 0;
  21263. });
  21264. if (this.values.length > 0) {
  21265. this.selectValue(0);
  21266. }
  21267. // create an array with the filtered datapoints. this will be loaded afterwards
  21268. this.dataPoints = [];
  21269. this.loaded = false;
  21270. this.onLoadCallback = undefined;
  21271. if (graph.animationPreload) {
  21272. this.loaded = false;
  21273. this.loadInBackground();
  21274. }
  21275. else {
  21276. this.loaded = true;
  21277. }
  21278. };
  21279. /**
  21280. * Return the label
  21281. * @return {string} label
  21282. */
  21283. Filter.prototype.isLoaded = function() {
  21284. return this.loaded;
  21285. };
  21286. /**
  21287. * Return the loaded progress
  21288. * @return {Number} percentage between 0 and 100
  21289. */
  21290. Filter.prototype.getLoadedProgress = function() {
  21291. var len = this.values.length;
  21292. var i = 0;
  21293. while (this.dataPoints[i]) {
  21294. i++;
  21295. }
  21296. return Math.round(i / len * 100);
  21297. };
  21298. /**
  21299. * Return the label
  21300. * @return {string} label
  21301. */
  21302. Filter.prototype.getLabel = function() {
  21303. return this.graph.filterLabel;
  21304. };
  21305. /**
  21306. * Return the columnIndex of the filter
  21307. * @return {Number} columnIndex
  21308. */
  21309. Filter.prototype.getColumn = function() {
  21310. return this.column;
  21311. };
  21312. /**
  21313. * Return the currently selected value. Returns undefined if there is no selection
  21314. * @return {*} value
  21315. */
  21316. Filter.prototype.getSelectedValue = function() {
  21317. if (this.index === undefined)
  21318. return undefined;
  21319. return this.values[this.index];
  21320. };
  21321. /**
  21322. * Retrieve all values of the filter
  21323. * @return {Array} values
  21324. */
  21325. Filter.prototype.getValues = function() {
  21326. return this.values;
  21327. };
  21328. /**
  21329. * Retrieve one value of the filter
  21330. * @param {Number} index
  21331. * @return {*} value
  21332. */
  21333. Filter.prototype.getValue = function(index) {
  21334. if (index >= this.values.length)
  21335. throw 'Error: index out of range';
  21336. return this.values[index];
  21337. };
  21338. /**
  21339. * Retrieve the (filtered) dataPoints for the currently selected filter index
  21340. * @param {Number} [index] (optional)
  21341. * @return {Array} dataPoints
  21342. */
  21343. Filter.prototype._getDataPoints = function(index) {
  21344. if (index === undefined)
  21345. index = this.index;
  21346. if (index === undefined)
  21347. return [];
  21348. var dataPoints;
  21349. if (this.dataPoints[index]) {
  21350. dataPoints = this.dataPoints[index];
  21351. }
  21352. else {
  21353. var f = {};
  21354. f.column = this.column;
  21355. f.value = this.values[index];
  21356. var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
  21357. dataPoints = this.graph._getDataPoints(dataView);
  21358. this.dataPoints[index] = dataPoints;
  21359. }
  21360. return dataPoints;
  21361. };
  21362. /**
  21363. * Set a callback function when the filter is fully loaded.
  21364. */
  21365. Filter.prototype.setOnLoadCallback = function(callback) {
  21366. this.onLoadCallback = callback;
  21367. };
  21368. /**
  21369. * Add a value to the list with available values for this filter
  21370. * No double entries will be created.
  21371. * @param {Number} index
  21372. */
  21373. Filter.prototype.selectValue = function(index) {
  21374. if (index >= this.values.length)
  21375. throw 'Error: index out of range';
  21376. this.index = index;
  21377. this.value = this.values[index];
  21378. };
  21379. /**
  21380. * Load all filtered rows in the background one by one
  21381. * Start this method without providing an index!
  21382. */
  21383. Filter.prototype.loadInBackground = function(index) {
  21384. if (index === undefined)
  21385. index = 0;
  21386. var frame = this.graph.frame;
  21387. if (index < this.values.length) {
  21388. var dataPointsTemp = this._getDataPoints(index);
  21389. //this.graph.redrawInfo(); // TODO: not neat
  21390. // create a progress box
  21391. if (frame.progress === undefined) {
  21392. frame.progress = document.createElement('DIV');
  21393. frame.progress.style.position = 'absolute';
  21394. frame.progress.style.color = 'gray';
  21395. frame.appendChild(frame.progress);
  21396. }
  21397. var progress = this.getLoadedProgress();
  21398. frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
  21399. // TODO: this is no nice solution...
  21400. frame.progress.style.bottom = Graph3d.px(60); // TODO: use height of slider
  21401. frame.progress.style.left = Graph3d.px(10);
  21402. var me = this;
  21403. setTimeout(function() {me.loadInBackground(index+1);}, 10);
  21404. this.loaded = false;
  21405. }
  21406. else {
  21407. this.loaded = true;
  21408. // remove the progress box
  21409. if (frame.progress !== undefined) {
  21410. frame.removeChild(frame.progress);
  21411. frame.progress = undefined;
  21412. }
  21413. if (this.onLoadCallback)
  21414. this.onLoadCallback();
  21415. }
  21416. };
  21417. /**
  21418. * @prototype StepNumber
  21419. * The class StepNumber is an iterator for Numbers. You provide a start and end
  21420. * value, and a best step size. StepNumber itself rounds to fixed values and
  21421. * a finds the step that best fits the provided step.
  21422. *
  21423. * If prettyStep is true, the step size is chosen as close as possible to the
  21424. * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
  21425. *
  21426. * Example usage:
  21427. * var step = new StepNumber(0, 10, 2.5, true);
  21428. * step.start();
  21429. * while (!step.end()) {
  21430. * alert(step.getCurrent());
  21431. * step.next();
  21432. * }
  21433. *
  21434. * Version: 1.0
  21435. *
  21436. * @param {Number} start The start value
  21437. * @param {Number} end The end value
  21438. * @param {Number} step Optional. Step size. Must be a positive value.
  21439. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  21440. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  21441. */
  21442. StepNumber = function (start, end, step, prettyStep) {
  21443. // set default values
  21444. this._start = 0;
  21445. this._end = 0;
  21446. this._step = 1;
  21447. this.prettyStep = true;
  21448. this.precision = 5;
  21449. this._current = 0;
  21450. this.setRange(start, end, step, prettyStep);
  21451. };
  21452. /**
  21453. * Set a new range: start, end and step.
  21454. *
  21455. * @param {Number} start The start value
  21456. * @param {Number} end The end value
  21457. * @param {Number} step Optional. Step size. Must be a positive value.
  21458. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  21459. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  21460. */
  21461. StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
  21462. this._start = start ? start : 0;
  21463. this._end = end ? end : 0;
  21464. this.setStep(step, prettyStep);
  21465. };
  21466. /**
  21467. * Set a new step size
  21468. * @param {Number} step New step size. Must be a positive value
  21469. * @param {boolean} prettyStep Optional. If true, the provided step is rounded
  21470. * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  21471. */
  21472. StepNumber.prototype.setStep = function(step, prettyStep) {
  21473. if (step === undefined || step <= 0)
  21474. return;
  21475. if (prettyStep !== undefined)
  21476. this.prettyStep = prettyStep;
  21477. if (this.prettyStep === true)
  21478. this._step = StepNumber.calculatePrettyStep(step);
  21479. else
  21480. this._step = step;
  21481. };
  21482. /**
  21483. * Calculate a nice step size, closest to the desired step size.
  21484. * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
  21485. * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
  21486. * @param {Number} step Desired step size
  21487. * @return {Number} Nice step size
  21488. */
  21489. StepNumber.calculatePrettyStep = function (step) {
  21490. var log10 = function (x) {return Math.log(x) / Math.LN10;};
  21491. // try three steps (multiple of 1, 2, or 5
  21492. var step1 = Math.pow(10, Math.round(log10(step))),
  21493. step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
  21494. step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
  21495. // choose the best step (closest to minimum step)
  21496. var prettyStep = step1;
  21497. if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
  21498. if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
  21499. // for safety
  21500. if (prettyStep <= 0) {
  21501. prettyStep = 1;
  21502. }
  21503. return prettyStep;
  21504. };
  21505. /**
  21506. * returns the current value of the step
  21507. * @return {Number} current value
  21508. */
  21509. StepNumber.prototype.getCurrent = function () {
  21510. return parseFloat(this._current.toPrecision(this.precision));
  21511. };
  21512. /**
  21513. * returns the current step size
  21514. * @return {Number} current step size
  21515. */
  21516. StepNumber.prototype.getStep = function () {
  21517. return this._step;
  21518. };
  21519. /**
  21520. * Set the current value to the largest value smaller than start, which
  21521. * is a multiple of the step size
  21522. */
  21523. StepNumber.prototype.start = function() {
  21524. this._current = this._start - this._start % this._step;
  21525. };
  21526. /**
  21527. * Do a step, add the step size to the current value
  21528. */
  21529. StepNumber.prototype.next = function () {
  21530. this._current += this._step;
  21531. };
  21532. /**
  21533. * Returns true whether the end is reached
  21534. * @return {boolean} True if the current value has passed the end value.
  21535. */
  21536. StepNumber.prototype.end = function () {
  21537. return (this._current > this._end);
  21538. };
  21539. /**
  21540. * @constructor Slider
  21541. *
  21542. * An html slider control with start/stop/prev/next buttons
  21543. * @param {Element} container The element where the slider will be created
  21544. * @param {Object} options Available options:
  21545. * {boolean} visible If true (default) the
  21546. * slider is visible.
  21547. */
  21548. function Slider(container, options) {
  21549. if (container === undefined) {
  21550. throw 'Error: No container element defined';
  21551. }
  21552. this.container = container;
  21553. this.visible = (options && options.visible != undefined) ? options.visible : true;
  21554. if (this.visible) {
  21555. this.frame = document.createElement('DIV');
  21556. //this.frame.style.backgroundColor = '#E5E5E5';
  21557. this.frame.style.width = '100%';
  21558. this.frame.style.position = 'relative';
  21559. this.container.appendChild(this.frame);
  21560. this.frame.prev = document.createElement('INPUT');
  21561. this.frame.prev.type = 'BUTTON';
  21562. this.frame.prev.value = 'Prev';
  21563. this.frame.appendChild(this.frame.prev);
  21564. this.frame.play = document.createElement('INPUT');
  21565. this.frame.play.type = 'BUTTON';
  21566. this.frame.play.value = 'Play';
  21567. this.frame.appendChild(this.frame.play);
  21568. this.frame.next = document.createElement('INPUT');
  21569. this.frame.next.type = 'BUTTON';
  21570. this.frame.next.value = 'Next';
  21571. this.frame.appendChild(this.frame.next);
  21572. this.frame.bar = document.createElement('INPUT');
  21573. this.frame.bar.type = 'BUTTON';
  21574. this.frame.bar.style.position = 'absolute';
  21575. this.frame.bar.style.border = '1px solid red';
  21576. this.frame.bar.style.width = '100px';
  21577. this.frame.bar.style.height = '6px';
  21578. this.frame.bar.style.borderRadius = '2px';
  21579. this.frame.bar.style.MozBorderRadius = '2px';
  21580. this.frame.bar.style.border = '1px solid #7F7F7F';
  21581. this.frame.bar.style.backgroundColor = '#E5E5E5';
  21582. this.frame.appendChild(this.frame.bar);
  21583. this.frame.slide = document.createElement('INPUT');
  21584. this.frame.slide.type = 'BUTTON';
  21585. this.frame.slide.style.margin = '0px';
  21586. this.frame.slide.value = ' ';
  21587. this.frame.slide.style.position = 'relative';
  21588. this.frame.slide.style.left = '-100px';
  21589. this.frame.appendChild(this.frame.slide);
  21590. // create events
  21591. var me = this;
  21592. this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
  21593. this.frame.prev.onclick = function (event) {me.prev(event);};
  21594. this.frame.play.onclick = function (event) {me.togglePlay(event);};
  21595. this.frame.next.onclick = function (event) {me.next(event);};
  21596. }
  21597. this.onChangeCallback = undefined;
  21598. this.values = [];
  21599. this.index = undefined;
  21600. this.playTimeout = undefined;
  21601. this.playInterval = 1000; // milliseconds
  21602. this.playLoop = true;
  21603. };
  21604. /**
  21605. * Select the previous index
  21606. */
  21607. Slider.prototype.prev = function() {
  21608. var index = this.getIndex();
  21609. if (index > 0) {
  21610. index--;
  21611. this.setIndex(index);
  21612. }
  21613. };
  21614. /**
  21615. * Select the next index
  21616. */
  21617. Slider.prototype.next = function() {
  21618. var index = this.getIndex();
  21619. if (index < this.values.length - 1) {
  21620. index++;
  21621. this.setIndex(index);
  21622. }
  21623. };
  21624. /**
  21625. * Select the next index
  21626. */
  21627. Slider.prototype.playNext = function() {
  21628. var start = new Date();
  21629. var index = this.getIndex();
  21630. if (index < this.values.length - 1) {
  21631. index++;
  21632. this.setIndex(index);
  21633. }
  21634. else if (this.playLoop) {
  21635. // jump to the start
  21636. index = 0;
  21637. this.setIndex(index);
  21638. }
  21639. var end = new Date();
  21640. var diff = (end - start);
  21641. // calculate how much time it to to set the index and to execute the callback
  21642. // function.
  21643. var interval = Math.max(this.playInterval - diff, 0);
  21644. // document.title = diff // TODO: cleanup
  21645. var me = this;
  21646. this.playTimeout = setTimeout(function() {me.playNext();}, interval);
  21647. };
  21648. /**
  21649. * Toggle start or stop playing
  21650. */
  21651. Slider.prototype.togglePlay = function() {
  21652. if (this.playTimeout === undefined) {
  21653. this.play();
  21654. } else {
  21655. this.stop();
  21656. }
  21657. };
  21658. /**
  21659. * Start playing
  21660. */
  21661. Slider.prototype.play = function() {
  21662. // Test whether already playing
  21663. if (this.playTimeout) return;
  21664. this.playNext();
  21665. if (this.frame) {
  21666. this.frame.play.value = 'Stop';
  21667. }
  21668. };
  21669. /**
  21670. * Stop playing
  21671. */
  21672. Slider.prototype.stop = function() {
  21673. clearInterval(this.playTimeout);
  21674. this.playTimeout = undefined;
  21675. if (this.frame) {
  21676. this.frame.play.value = 'Play';
  21677. }
  21678. };
  21679. /**
  21680. * Set a callback function which will be triggered when the value of the
  21681. * slider bar has changed.
  21682. */
  21683. Slider.prototype.setOnChangeCallback = function(callback) {
  21684. this.onChangeCallback = callback;
  21685. };
  21686. /**
  21687. * Set the interval for playing the list
  21688. * @param {Number} interval The interval in milliseconds
  21689. */
  21690. Slider.prototype.setPlayInterval = function(interval) {
  21691. this.playInterval = interval;
  21692. };
  21693. /**
  21694. * Retrieve the current play interval
  21695. * @return {Number} interval The interval in milliseconds
  21696. */
  21697. Slider.prototype.getPlayInterval = function(interval) {
  21698. return this.playInterval;
  21699. };
  21700. /**
  21701. * Set looping on or off
  21702. * @pararm {boolean} doLoop If true, the slider will jump to the start when
  21703. * the end is passed, and will jump to the end
  21704. * when the start is passed.
  21705. */
  21706. Slider.prototype.setPlayLoop = function(doLoop) {
  21707. this.playLoop = doLoop;
  21708. };
  21709. /**
  21710. * Execute the onchange callback function
  21711. */
  21712. Slider.prototype.onChange = function() {
  21713. if (this.onChangeCallback !== undefined) {
  21714. this.onChangeCallback();
  21715. }
  21716. };
  21717. /**
  21718. * redraw the slider on the correct place
  21719. */
  21720. Slider.prototype.redraw = function() {
  21721. if (this.frame) {
  21722. // resize the bar
  21723. this.frame.bar.style.top = (this.frame.clientHeight/2 -
  21724. this.frame.bar.offsetHeight/2) + 'px';
  21725. this.frame.bar.style.width = (this.frame.clientWidth -
  21726. this.frame.prev.clientWidth -
  21727. this.frame.play.clientWidth -
  21728. this.frame.next.clientWidth - 30) + 'px';
  21729. // position the slider button
  21730. var left = this.indexToLeft(this.index);
  21731. this.frame.slide.style.left = (left) + 'px';
  21732. }
  21733. };
  21734. /**
  21735. * Set the list with values for the slider
  21736. * @param {Array} values A javascript array with values (any type)
  21737. */
  21738. Slider.prototype.setValues = function(values) {
  21739. this.values = values;
  21740. if (this.values.length > 0)
  21741. this.setIndex(0);
  21742. else
  21743. this.index = undefined;
  21744. };
  21745. /**
  21746. * Select a value by its index
  21747. * @param {Number} index
  21748. */
  21749. Slider.prototype.setIndex = function(index) {
  21750. if (index < this.values.length) {
  21751. this.index = index;
  21752. this.redraw();
  21753. this.onChange();
  21754. }
  21755. else {
  21756. throw 'Error: index out of range';
  21757. }
  21758. };
  21759. /**
  21760. * retrieve the index of the currently selected vaue
  21761. * @return {Number} index
  21762. */
  21763. Slider.prototype.getIndex = function() {
  21764. return this.index;
  21765. };
  21766. /**
  21767. * retrieve the currently selected value
  21768. * @return {*} value
  21769. */
  21770. Slider.prototype.get = function() {
  21771. return this.values[this.index];
  21772. };
  21773. Slider.prototype._onMouseDown = function(event) {
  21774. // only react on left mouse button down
  21775. var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  21776. if (!leftButtonDown) return;
  21777. this.startClientX = event.clientX;
  21778. this.startSlideX = parseFloat(this.frame.slide.style.left);
  21779. this.frame.style.cursor = 'move';
  21780. // add event listeners to handle moving the contents
  21781. // we store the function onmousemove and onmouseup in the graph, so we can
  21782. // remove the eventlisteners lateron in the function mouseUp()
  21783. var me = this;
  21784. this.onmousemove = function (event) {me._onMouseMove(event);};
  21785. this.onmouseup = function (event) {me._onMouseUp(event);};
  21786. G3DaddEventListener(document, 'mousemove', this.onmousemove);
  21787. G3DaddEventListener(document, 'mouseup', this.onmouseup);
  21788. G3DpreventDefault(event);
  21789. };
  21790. Slider.prototype.leftToIndex = function (left) {
  21791. var width = parseFloat(this.frame.bar.style.width) -
  21792. this.frame.slide.clientWidth - 10;
  21793. var x = left - 3;
  21794. var index = Math.round(x / width * (this.values.length-1));
  21795. if (index < 0) index = 0;
  21796. if (index > this.values.length-1) index = this.values.length-1;
  21797. return index;
  21798. };
  21799. Slider.prototype.indexToLeft = function (index) {
  21800. var width = parseFloat(this.frame.bar.style.width) -
  21801. this.frame.slide.clientWidth - 10;
  21802. var x = index / (this.values.length-1) * width;
  21803. var left = x + 3;
  21804. return left;
  21805. };
  21806. Slider.prototype._onMouseMove = function (event) {
  21807. var diff = event.clientX - this.startClientX;
  21808. var x = this.startSlideX + diff;
  21809. var index = this.leftToIndex(x);
  21810. this.setIndex(index);
  21811. G3DpreventDefault();
  21812. };
  21813. Slider.prototype._onMouseUp = function (event) {
  21814. this.frame.style.cursor = 'auto';
  21815. // remove event listeners
  21816. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  21817. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  21818. G3DpreventDefault();
  21819. };
  21820. /**--------------------------------------------------------------------------**/
  21821. /**
  21822. * Retrieve the absolute left value of a DOM element
  21823. * @param {Element} elem A dom element, for example a div
  21824. * @return {Number} left The absolute left position of this element
  21825. * in the browser page.
  21826. */
  21827. getAbsoluteLeft = function(elem) {
  21828. var left = 0;
  21829. while( elem !== null ) {
  21830. left += elem.offsetLeft;
  21831. left -= elem.scrollLeft;
  21832. elem = elem.offsetParent;
  21833. }
  21834. return left;
  21835. };
  21836. /**
  21837. * Retrieve the absolute top value of a DOM element
  21838. * @param {Element} elem A dom element, for example a div
  21839. * @return {Number} top The absolute top position of this element
  21840. * in the browser page.
  21841. */
  21842. getAbsoluteTop = function(elem) {
  21843. var top = 0;
  21844. while( elem !== null ) {
  21845. top += elem.offsetTop;
  21846. top -= elem.scrollTop;
  21847. elem = elem.offsetParent;
  21848. }
  21849. return top;
  21850. };
  21851. /**
  21852. * Get the horizontal mouse position from a mouse event
  21853. * @param {Event} event
  21854. * @return {Number} mouse x
  21855. */
  21856. getMouseX = function(event) {
  21857. if ('clientX' in event) return event.clientX;
  21858. return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
  21859. };
  21860. /**
  21861. * Get the vertical mouse position from a mouse event
  21862. * @param {Event} event
  21863. * @return {Number} mouse y
  21864. */
  21865. getMouseY = function(event) {
  21866. if ('clientY' in event) return event.clientY;
  21867. return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
  21868. };
  21869. /**
  21870. * vis.js module exports
  21871. */
  21872. var vis = {
  21873. moment: moment,
  21874. util: util,
  21875. DOMutil: DOMutil,
  21876. DataSet: DataSet,
  21877. DataView: DataView,
  21878. Timeline: Timeline,
  21879. Graph2d: Graph2d,
  21880. timeline: {
  21881. DataStep: DataStep,
  21882. Range: Range,
  21883. stack: stack,
  21884. TimeStep: TimeStep,
  21885. components: {
  21886. items: {
  21887. Item: Item,
  21888. ItemBox: ItemBox,
  21889. ItemPoint: ItemPoint,
  21890. ItemRange: ItemRange
  21891. },
  21892. Component: Component,
  21893. CurrentTime: CurrentTime,
  21894. CustomTime: CustomTime,
  21895. DataAxis: DataAxis,
  21896. GraphGroup: GraphGroup,
  21897. Group: Group,
  21898. ItemSet: ItemSet,
  21899. Legend: Legend,
  21900. LineGraph: LineGraph,
  21901. TimeAxis: TimeAxis
  21902. }
  21903. },
  21904. Network: Network,
  21905. network: {
  21906. Edge: Edge,
  21907. Groups: Groups,
  21908. Images: Images,
  21909. Node: Node,
  21910. Popup: Popup
  21911. },
  21912. // Deprecated since v3.0.0
  21913. Graph: function () {
  21914. throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
  21915. },
  21916. Graph3d: Graph3d
  21917. };
  21918. /**
  21919. * CommonJS module exports
  21920. */
  21921. if (typeof exports !== 'undefined') {
  21922. exports = vis;
  21923. }
  21924. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  21925. module.exports = vis;
  21926. }
  21927. /**
  21928. * AMD module exports
  21929. */
  21930. if (typeof(define) === 'function') {
  21931. define(function () {
  21932. return vis;
  21933. });
  21934. }
  21935. /**
  21936. * Window exports
  21937. */
  21938. if (typeof window !== 'undefined') {
  21939. // attach the module to the window, load as a regular javascript file
  21940. window['vis'] = vis;
  21941. }
  21942. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  21943. /**
  21944. * Expose `Emitter`.
  21945. */
  21946. module.exports = Emitter;
  21947. /**
  21948. * Initialize a new `Emitter`.
  21949. *
  21950. * @api public
  21951. */
  21952. function Emitter(obj) {
  21953. if (obj) return mixin(obj);
  21954. };
  21955. /**
  21956. * Mixin the emitter properties.
  21957. *
  21958. * @param {Object} obj
  21959. * @return {Object}
  21960. * @api private
  21961. */
  21962. function mixin(obj) {
  21963. for (var key in Emitter.prototype) {
  21964. obj[key] = Emitter.prototype[key];
  21965. }
  21966. return obj;
  21967. }
  21968. /**
  21969. * Listen on the given `event` with `fn`.
  21970. *
  21971. * @param {String} event
  21972. * @param {Function} fn
  21973. * @return {Emitter}
  21974. * @api public
  21975. */
  21976. Emitter.prototype.on =
  21977. Emitter.prototype.addEventListener = function(event, fn){
  21978. this._callbacks = this._callbacks || {};
  21979. (this._callbacks[event] = this._callbacks[event] || [])
  21980. .push(fn);
  21981. return this;
  21982. };
  21983. /**
  21984. * Adds an `event` listener that will be invoked a single
  21985. * time then automatically removed.
  21986. *
  21987. * @param {String} event
  21988. * @param {Function} fn
  21989. * @return {Emitter}
  21990. * @api public
  21991. */
  21992. Emitter.prototype.once = function(event, fn){
  21993. var self = this;
  21994. this._callbacks = this._callbacks || {};
  21995. function on() {
  21996. self.off(event, on);
  21997. fn.apply(this, arguments);
  21998. }
  21999. on.fn = fn;
  22000. this.on(event, on);
  22001. return this;
  22002. };
  22003. /**
  22004. * Remove the given callback for `event` or all
  22005. * registered callbacks.
  22006. *
  22007. * @param {String} event
  22008. * @param {Function} fn
  22009. * @return {Emitter}
  22010. * @api public
  22011. */
  22012. Emitter.prototype.off =
  22013. Emitter.prototype.removeListener =
  22014. Emitter.prototype.removeAllListeners =
  22015. Emitter.prototype.removeEventListener = function(event, fn){
  22016. this._callbacks = this._callbacks || {};
  22017. // all
  22018. if (0 == arguments.length) {
  22019. this._callbacks = {};
  22020. return this;
  22021. }
  22022. // specific event
  22023. var callbacks = this._callbacks[event];
  22024. if (!callbacks) return this;
  22025. // remove all handlers
  22026. if (1 == arguments.length) {
  22027. delete this._callbacks[event];
  22028. return this;
  22029. }
  22030. // remove specific handler
  22031. var cb;
  22032. for (var i = 0; i < callbacks.length; i++) {
  22033. cb = callbacks[i];
  22034. if (cb === fn || cb.fn === fn) {
  22035. callbacks.splice(i, 1);
  22036. break;
  22037. }
  22038. }
  22039. return this;
  22040. };
  22041. /**
  22042. * Emit `event` with the given args.
  22043. *
  22044. * @param {String} event
  22045. * @param {Mixed} ...
  22046. * @return {Emitter}
  22047. */
  22048. Emitter.prototype.emit = function(event){
  22049. this._callbacks = this._callbacks || {};
  22050. var args = [].slice.call(arguments, 1)
  22051. , callbacks = this._callbacks[event];
  22052. if (callbacks) {
  22053. callbacks = callbacks.slice(0);
  22054. for (var i = 0, len = callbacks.length; i < len; ++i) {
  22055. callbacks[i].apply(this, args);
  22056. }
  22057. }
  22058. return this;
  22059. };
  22060. /**
  22061. * Return array of callbacks for `event`.
  22062. *
  22063. * @param {String} event
  22064. * @return {Array}
  22065. * @api public
  22066. */
  22067. Emitter.prototype.listeners = function(event){
  22068. this._callbacks = this._callbacks || {};
  22069. return this._callbacks[event] || [];
  22070. };
  22071. /**
  22072. * Check if this emitter has `event` handlers.
  22073. *
  22074. * @param {String} event
  22075. * @return {Boolean}
  22076. * @api public
  22077. */
  22078. Emitter.prototype.hasListeners = function(event){
  22079. return !! this.listeners(event).length;
  22080. };
  22081. },{}],3:[function(require,module,exports){
  22082. /*! Hammer.JS - v1.0.5 - 2013-04-07
  22083. * http://eightmedia.github.com/hammer.js
  22084. *
  22085. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  22086. * Licensed under the MIT license */
  22087. (function(window, undefined) {
  22088. 'use strict';
  22089. /**
  22090. * Hammer
  22091. * use this to create instances
  22092. * @param {HTMLElement} element
  22093. * @param {Object} options
  22094. * @returns {Hammer.Instance}
  22095. * @constructor
  22096. */
  22097. var Hammer = function(element, options) {
  22098. return new Hammer.Instance(element, options || {});
  22099. };
  22100. // default settings
  22101. Hammer.defaults = {
  22102. // add styles and attributes to the element to prevent the browser from doing
  22103. // its native behavior. this doesnt prevent the scrolling, but cancels
  22104. // the contextmenu, tap highlighting etc
  22105. // set to false to disable this
  22106. stop_browser_behavior: {
  22107. // this also triggers onselectstart=false for IE
  22108. userSelect: 'none',
  22109. // this makes the element blocking in IE10 >, you could experiment with the value
  22110. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  22111. touchAction: 'none',
  22112. touchCallout: 'none',
  22113. contentZooming: 'none',
  22114. userDrag: 'none',
  22115. tapHighlightColor: 'rgba(0,0,0,0)'
  22116. }
  22117. // more settings are defined per gesture at gestures.js
  22118. };
  22119. // detect touchevents
  22120. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  22121. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  22122. // dont use mouseevents on mobile devices
  22123. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  22124. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  22125. // eventtypes per touchevent (start, move, end)
  22126. // are filled by Hammer.event.determineEventTypes on setup
  22127. Hammer.EVENT_TYPES = {};
  22128. // direction defines
  22129. Hammer.DIRECTION_DOWN = 'down';
  22130. Hammer.DIRECTION_LEFT = 'left';
  22131. Hammer.DIRECTION_UP = 'up';
  22132. Hammer.DIRECTION_RIGHT = 'right';
  22133. // pointer type
  22134. Hammer.POINTER_MOUSE = 'mouse';
  22135. Hammer.POINTER_TOUCH = 'touch';
  22136. Hammer.POINTER_PEN = 'pen';
  22137. // touch event defines
  22138. Hammer.EVENT_START = 'start';
  22139. Hammer.EVENT_MOVE = 'move';
  22140. Hammer.EVENT_END = 'end';
  22141. // hammer document where the base events are added at
  22142. Hammer.DOCUMENT = document;
  22143. // plugins namespace
  22144. Hammer.plugins = {};
  22145. // if the window events are set...
  22146. Hammer.READY = false;
  22147. /**
  22148. * setup events to detect gestures on the document
  22149. */
  22150. function setup() {
  22151. if(Hammer.READY) {
  22152. return;
  22153. }
  22154. // find what eventtypes we add listeners to
  22155. Hammer.event.determineEventTypes();
  22156. // Register all gestures inside Hammer.gestures
  22157. for(var name in Hammer.gestures) {
  22158. if(Hammer.gestures.hasOwnProperty(name)) {
  22159. Hammer.detection.register(Hammer.gestures[name]);
  22160. }
  22161. }
  22162. // Add touch events on the document
  22163. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  22164. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  22165. // Hammer is ready...!
  22166. Hammer.READY = true;
  22167. }
  22168. /**
  22169. * create new hammer instance
  22170. * all methods should return the instance itself, so it is chainable.
  22171. * @param {HTMLElement} element
  22172. * @param {Object} [options={}]
  22173. * @returns {Hammer.Instance}
  22174. * @constructor
  22175. */
  22176. Hammer.Instance = function(element, options) {
  22177. var self = this;
  22178. // setup HammerJS window events and register all gestures
  22179. // this also sets up the default options
  22180. setup();
  22181. this.element = element;
  22182. // start/stop detection option
  22183. this.enabled = true;
  22184. // merge options
  22185. this.options = Hammer.utils.extend(
  22186. Hammer.utils.extend({}, Hammer.defaults),
  22187. options || {});
  22188. // add some css to the element to prevent the browser from doing its native behavoir
  22189. if(this.options.stop_browser_behavior) {
  22190. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  22191. }
  22192. // start detection on touchstart
  22193. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  22194. if(self.enabled) {
  22195. Hammer.detection.startDetect(self, ev);
  22196. }
  22197. });
  22198. // return instance
  22199. return this;
  22200. };
  22201. Hammer.Instance.prototype = {
  22202. /**
  22203. * bind events to the instance
  22204. * @param {String} gesture
  22205. * @param {Function} handler
  22206. * @returns {Hammer.Instance}
  22207. */
  22208. on: function onEvent(gesture, handler){
  22209. var gestures = gesture.split(' ');
  22210. for(var t=0; t<gestures.length; t++) {
  22211. this.element.addEventListener(gestures[t], handler, false);
  22212. }
  22213. return this;
  22214. },
  22215. /**
  22216. * unbind events to the instance
  22217. * @param {String} gesture
  22218. * @param {Function} handler
  22219. * @returns {Hammer.Instance}
  22220. */
  22221. off: function offEvent(gesture, handler){
  22222. var gestures = gesture.split(' ');
  22223. for(var t=0; t<gestures.length; t++) {
  22224. this.element.removeEventListener(gestures[t], handler, false);
  22225. }
  22226. return this;
  22227. },
  22228. /**
  22229. * trigger gesture event
  22230. * @param {String} gesture
  22231. * @param {Object} eventData
  22232. * @returns {Hammer.Instance}
  22233. */
  22234. trigger: function triggerEvent(gesture, eventData){
  22235. // create DOM event
  22236. var event = Hammer.DOCUMENT.createEvent('Event');
  22237. event.initEvent(gesture, true, true);
  22238. event.gesture = eventData;
  22239. // trigger on the target if it is in the instance element,
  22240. // this is for event delegation tricks
  22241. var element = this.element;
  22242. if(Hammer.utils.hasParent(eventData.target, element)) {
  22243. element = eventData.target;
  22244. }
  22245. element.dispatchEvent(event);
  22246. return this;
  22247. },
  22248. /**
  22249. * enable of disable hammer.js detection
  22250. * @param {Boolean} state
  22251. * @returns {Hammer.Instance}
  22252. */
  22253. enable: function enable(state) {
  22254. this.enabled = state;
  22255. return this;
  22256. }
  22257. };
  22258. /**
  22259. * this holds the last move event,
  22260. * used to fix empty touchend issue
  22261. * see the onTouch event for an explanation
  22262. * @type {Object}
  22263. */
  22264. var last_move_event = null;
  22265. /**
  22266. * when the mouse is hold down, this is true
  22267. * @type {Boolean}
  22268. */
  22269. var enable_detect = false;
  22270. /**
  22271. * when touch events have been fired, this is true
  22272. * @type {Boolean}
  22273. */
  22274. var touch_triggered = false;
  22275. Hammer.event = {
  22276. /**
  22277. * simple addEventListener
  22278. * @param {HTMLElement} element
  22279. * @param {String} type
  22280. * @param {Function} handler
  22281. */
  22282. bindDom: function(element, type, handler) {
  22283. var types = type.split(' ');
  22284. for(var t=0; t<types.length; t++) {
  22285. element.addEventListener(types[t], handler, false);
  22286. }
  22287. },
  22288. /**
  22289. * touch events with mouse fallback
  22290. * @param {HTMLElement} element
  22291. * @param {String} eventType like Hammer.EVENT_MOVE
  22292. * @param {Function} handler
  22293. */
  22294. onTouch: function onTouch(element, eventType, handler) {
  22295. var self = this;
  22296. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  22297. var sourceEventType = ev.type.toLowerCase();
  22298. // onmouseup, but when touchend has been fired we do nothing.
  22299. // this is for touchdevices which also fire a mouseup on touchend
  22300. if(sourceEventType.match(/mouse/) && touch_triggered) {
  22301. return;
  22302. }
  22303. // mousebutton must be down or a touch event
  22304. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  22305. sourceEventType.match(/pointerdown/) || // pointerevents touch
  22306. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  22307. ){
  22308. enable_detect = true;
  22309. }
  22310. // we are in a touch event, set the touch triggered bool to true,
  22311. // this for the conflicts that may occur on ios and android
  22312. if(sourceEventType.match(/touch|pointer/)) {
  22313. touch_triggered = true;
  22314. }
  22315. // count the total touches on the screen
  22316. var count_touches = 0;
  22317. // when touch has been triggered in this detection session
  22318. // and we are now handling a mouse event, we stop that to prevent conflicts
  22319. if(enable_detect) {
  22320. // update pointerevent
  22321. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  22322. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  22323. }
  22324. // touch
  22325. else if(sourceEventType.match(/touch/)) {
  22326. count_touches = ev.touches.length;
  22327. }
  22328. // mouse
  22329. else if(!touch_triggered) {
  22330. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  22331. }
  22332. // if we are in a end event, but when we remove one touch and
  22333. // we still have enough, set eventType to move
  22334. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  22335. eventType = Hammer.EVENT_MOVE;
  22336. }
  22337. // no touches, force the end event
  22338. else if(!count_touches) {
  22339. eventType = Hammer.EVENT_END;
  22340. }
  22341. // because touchend has no touches, and we often want to use these in our gestures,
  22342. // we send the last move event as our eventData in touchend
  22343. if(!count_touches && last_move_event !== null) {
  22344. ev = last_move_event;
  22345. }
  22346. // store the last move event
  22347. else {
  22348. last_move_event = ev;
  22349. }
  22350. // trigger the handler
  22351. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  22352. // remove pointerevent from list
  22353. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  22354. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  22355. }
  22356. }
  22357. //debug(sourceEventType +" "+ eventType);
  22358. // on the end we reset everything
  22359. if(!count_touches) {
  22360. last_move_event = null;
  22361. enable_detect = false;
  22362. touch_triggered = false;
  22363. Hammer.PointerEvent.reset();
  22364. }
  22365. });
  22366. },
  22367. /**
  22368. * we have different events for each device/browser
  22369. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  22370. */
  22371. determineEventTypes: function determineEventTypes() {
  22372. // determine the eventtype we want to set
  22373. var types;
  22374. // pointerEvents magic
  22375. if(Hammer.HAS_POINTEREVENTS) {
  22376. types = Hammer.PointerEvent.getEvents();
  22377. }
  22378. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  22379. else if(Hammer.NO_MOUSEEVENTS) {
  22380. types = [
  22381. 'touchstart',
  22382. 'touchmove',
  22383. 'touchend touchcancel'];
  22384. }
  22385. // for non pointer events browsers and mixed browsers,
  22386. // like chrome on windows8 touch laptop
  22387. else {
  22388. types = [
  22389. 'touchstart mousedown',
  22390. 'touchmove mousemove',
  22391. 'touchend touchcancel mouseup'];
  22392. }
  22393. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  22394. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  22395. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  22396. },
  22397. /**
  22398. * create touchlist depending on the event
  22399. * @param {Object} ev
  22400. * @param {String} eventType used by the fakemultitouch plugin
  22401. */
  22402. getTouchList: function getTouchList(ev/*, eventType*/) {
  22403. // get the fake pointerEvent touchlist
  22404. if(Hammer.HAS_POINTEREVENTS) {
  22405. return Hammer.PointerEvent.getTouchList();
  22406. }
  22407. // get the touchlist
  22408. else if(ev.touches) {
  22409. return ev.touches;
  22410. }
  22411. // make fake touchlist from mouse position
  22412. else {
  22413. return [{
  22414. identifier: 1,
  22415. pageX: ev.pageX,
  22416. pageY: ev.pageY,
  22417. target: ev.target
  22418. }];
  22419. }
  22420. },
  22421. /**
  22422. * collect event data for Hammer js
  22423. * @param {HTMLElement} element
  22424. * @param {String} eventType like Hammer.EVENT_MOVE
  22425. * @param {Object} eventData
  22426. */
  22427. collectEventData: function collectEventData(element, eventType, ev) {
  22428. var touches = this.getTouchList(ev, eventType);
  22429. // find out pointerType
  22430. var pointerType = Hammer.POINTER_TOUCH;
  22431. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  22432. pointerType = Hammer.POINTER_MOUSE;
  22433. }
  22434. return {
  22435. center : Hammer.utils.getCenter(touches),
  22436. timeStamp : new Date().getTime(),
  22437. target : ev.target,
  22438. touches : touches,
  22439. eventType : eventType,
  22440. pointerType : pointerType,
  22441. srcEvent : ev,
  22442. /**
  22443. * prevent the browser default actions
  22444. * mostly used to disable scrolling of the browser
  22445. */
  22446. preventDefault: function() {
  22447. if(this.srcEvent.preventManipulation) {
  22448. this.srcEvent.preventManipulation();
  22449. }
  22450. if(this.srcEvent.preventDefault) {
  22451. this.srcEvent.preventDefault();
  22452. }
  22453. },
  22454. /**
  22455. * stop bubbling the event up to its parents
  22456. */
  22457. stopPropagation: function() {
  22458. this.srcEvent.stopPropagation();
  22459. },
  22460. /**
  22461. * immediately stop gesture detection
  22462. * might be useful after a swipe was detected
  22463. * @return {*}
  22464. */
  22465. stopDetect: function() {
  22466. return Hammer.detection.stopDetect();
  22467. }
  22468. };
  22469. }
  22470. };
  22471. Hammer.PointerEvent = {
  22472. /**
  22473. * holds all pointers
  22474. * @type {Object}
  22475. */
  22476. pointers: {},
  22477. /**
  22478. * get a list of pointers
  22479. * @returns {Array} touchlist
  22480. */
  22481. getTouchList: function() {
  22482. var self = this;
  22483. var touchlist = [];
  22484. // we can use forEach since pointerEvents only is in IE10
  22485. Object.keys(self.pointers).sort().forEach(function(id) {
  22486. touchlist.push(self.pointers[id]);
  22487. });
  22488. return touchlist;
  22489. },
  22490. /**
  22491. * update the position of a pointer
  22492. * @param {String} type Hammer.EVENT_END
  22493. * @param {Object} pointerEvent
  22494. */
  22495. updatePointer: function(type, pointerEvent) {
  22496. if(type == Hammer.EVENT_END) {
  22497. this.pointers = {};
  22498. }
  22499. else {
  22500. pointerEvent.identifier = pointerEvent.pointerId;
  22501. this.pointers[pointerEvent.pointerId] = pointerEvent;
  22502. }
  22503. return Object.keys(this.pointers).length;
  22504. },
  22505. /**
  22506. * check if ev matches pointertype
  22507. * @param {String} pointerType Hammer.POINTER_MOUSE
  22508. * @param {PointerEvent} ev
  22509. */
  22510. matchType: function(pointerType, ev) {
  22511. if(!ev.pointerType) {
  22512. return false;
  22513. }
  22514. var types = {};
  22515. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  22516. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  22517. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  22518. return types[pointerType];
  22519. },
  22520. /**
  22521. * get events
  22522. */
  22523. getEvents: function() {
  22524. return [
  22525. 'pointerdown MSPointerDown',
  22526. 'pointermove MSPointerMove',
  22527. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  22528. ];
  22529. },
  22530. /**
  22531. * reset the list
  22532. */
  22533. reset: function() {
  22534. this.pointers = {};
  22535. }
  22536. };
  22537. Hammer.utils = {
  22538. /**
  22539. * extend method,
  22540. * also used for cloning when dest is an empty object
  22541. * @param {Object} dest
  22542. * @param {Object} src
  22543. * @parm {Boolean} merge do a merge
  22544. * @returns {Object} dest
  22545. */
  22546. extend: function extend(dest, src, merge) {
  22547. for (var key in src) {
  22548. if(dest[key] !== undefined && merge) {
  22549. continue;
  22550. }
  22551. dest[key] = src[key];
  22552. }
  22553. return dest;
  22554. },
  22555. /**
  22556. * find if a node is in the given parent
  22557. * used for event delegation tricks
  22558. * @param {HTMLElement} node
  22559. * @param {HTMLElement} parent
  22560. * @returns {boolean} has_parent
  22561. */
  22562. hasParent: function(node, parent) {
  22563. while(node){
  22564. if(node == parent) {
  22565. return true;
  22566. }
  22567. node = node.parentNode;
  22568. }
  22569. return false;
  22570. },
  22571. /**
  22572. * get the center of all the touches
  22573. * @param {Array} touches
  22574. * @returns {Object} center
  22575. */
  22576. getCenter: function getCenter(touches) {
  22577. var valuesX = [], valuesY = [];
  22578. for(var t= 0,len=touches.length; t<len; t++) {
  22579. valuesX.push(touches[t].pageX);
  22580. valuesY.push(touches[t].pageY);
  22581. }
  22582. return {
  22583. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  22584. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  22585. };
  22586. },
  22587. /**
  22588. * calculate the velocity between two points
  22589. * @param {Number} delta_time
  22590. * @param {Number} delta_x
  22591. * @param {Number} delta_y
  22592. * @returns {Object} velocity
  22593. */
  22594. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  22595. return {
  22596. x: Math.abs(delta_x / delta_time) || 0,
  22597. y: Math.abs(delta_y / delta_time) || 0
  22598. };
  22599. },
  22600. /**
  22601. * calculate the angle between two coordinates
  22602. * @param {Touch} touch1
  22603. * @param {Touch} touch2
  22604. * @returns {Number} angle
  22605. */
  22606. getAngle: function getAngle(touch1, touch2) {
  22607. var y = touch2.pageY - touch1.pageY,
  22608. x = touch2.pageX - touch1.pageX;
  22609. return Math.atan2(y, x) * 180 / Math.PI;
  22610. },
  22611. /**
  22612. * angle to direction define
  22613. * @param {Touch} touch1
  22614. * @param {Touch} touch2
  22615. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  22616. */
  22617. getDirection: function getDirection(touch1, touch2) {
  22618. var x = Math.abs(touch1.pageX - touch2.pageX),
  22619. y = Math.abs(touch1.pageY - touch2.pageY);
  22620. if(x >= y) {
  22621. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  22622. }
  22623. else {
  22624. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  22625. }
  22626. },
  22627. /**
  22628. * calculate the distance between two touches
  22629. * @param {Touch} touch1
  22630. * @param {Touch} touch2
  22631. * @returns {Number} distance
  22632. */
  22633. getDistance: function getDistance(touch1, touch2) {
  22634. var x = touch2.pageX - touch1.pageX,
  22635. y = touch2.pageY - touch1.pageY;
  22636. return Math.sqrt((x*x) + (y*y));
  22637. },
  22638. /**
  22639. * calculate the scale factor between two touchLists (fingers)
  22640. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  22641. * @param {Array} start
  22642. * @param {Array} end
  22643. * @returns {Number} scale
  22644. */
  22645. getScale: function getScale(start, end) {
  22646. // need two fingers...
  22647. if(start.length >= 2 && end.length >= 2) {
  22648. return this.getDistance(end[0], end[1]) /
  22649. this.getDistance(start[0], start[1]);
  22650. }
  22651. return 1;
  22652. },
  22653. /**
  22654. * calculate the rotation degrees between two touchLists (fingers)
  22655. * @param {Array} start
  22656. * @param {Array} end
  22657. * @returns {Number} rotation
  22658. */
  22659. getRotation: function getRotation(start, end) {
  22660. // need two fingers
  22661. if(start.length >= 2 && end.length >= 2) {
  22662. return this.getAngle(end[1], end[0]) -
  22663. this.getAngle(start[1], start[0]);
  22664. }
  22665. return 0;
  22666. },
  22667. /**
  22668. * boolean if the direction is vertical
  22669. * @param {String} direction
  22670. * @returns {Boolean} is_vertical
  22671. */
  22672. isVertical: function isVertical(direction) {
  22673. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  22674. },
  22675. /**
  22676. * stop browser default behavior with css props
  22677. * @param {HtmlElement} element
  22678. * @param {Object} css_props
  22679. */
  22680. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  22681. var prop,
  22682. vendors = ['webkit','khtml','moz','ms','o',''];
  22683. if(!css_props || !element.style) {
  22684. return;
  22685. }
  22686. // with css properties for modern browsers
  22687. for(var i = 0; i < vendors.length; i++) {
  22688. for(var p in css_props) {
  22689. if(css_props.hasOwnProperty(p)) {
  22690. prop = p;
  22691. // vender prefix at the property
  22692. if(vendors[i]) {
  22693. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  22694. }
  22695. // set the style
  22696. element.style[prop] = css_props[p];
  22697. }
  22698. }
  22699. }
  22700. // also the disable onselectstart
  22701. if(css_props.userSelect == 'none') {
  22702. element.onselectstart = function() {
  22703. return false;
  22704. };
  22705. }
  22706. }
  22707. };
  22708. Hammer.detection = {
  22709. // contains all registred Hammer.gestures in the correct order
  22710. gestures: [],
  22711. // data of the current Hammer.gesture detection session
  22712. current: null,
  22713. // the previous Hammer.gesture session data
  22714. // is a full clone of the previous gesture.current object
  22715. previous: null,
  22716. // when this becomes true, no gestures are fired
  22717. stopped: false,
  22718. /**
  22719. * start Hammer.gesture detection
  22720. * @param {Hammer.Instance} inst
  22721. * @param {Object} eventData
  22722. */
  22723. startDetect: function startDetect(inst, eventData) {
  22724. // already busy with a Hammer.gesture detection on an element
  22725. if(this.current) {
  22726. return;
  22727. }
  22728. this.stopped = false;
  22729. this.current = {
  22730. inst : inst, // reference to HammerInstance we're working for
  22731. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  22732. lastEvent : false, // last eventData
  22733. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  22734. };
  22735. this.detect(eventData);
  22736. },
  22737. /**
  22738. * Hammer.gesture detection
  22739. * @param {Object} eventData
  22740. * @param {Object} eventData
  22741. */
  22742. detect: function detect(eventData) {
  22743. if(!this.current || this.stopped) {
  22744. return;
  22745. }
  22746. // extend event data with calculations about scale, distance etc
  22747. eventData = this.extendEventData(eventData);
  22748. // instance options
  22749. var inst_options = this.current.inst.options;
  22750. // call Hammer.gesture handlers
  22751. for(var g=0,len=this.gestures.length; g<len; g++) {
  22752. var gesture = this.gestures[g];
  22753. // only when the instance options have enabled this gesture
  22754. if(!this.stopped && inst_options[gesture.name] !== false) {
  22755. // if a handler returns false, we stop with the detection
  22756. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  22757. this.stopDetect();
  22758. break;
  22759. }
  22760. }
  22761. }
  22762. // store as previous event event
  22763. if(this.current) {
  22764. this.current.lastEvent = eventData;
  22765. }
  22766. // endevent, but not the last touch, so dont stop
  22767. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  22768. this.stopDetect();
  22769. }
  22770. return eventData;
  22771. },
  22772. /**
  22773. * clear the Hammer.gesture vars
  22774. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  22775. * to stop other Hammer.gestures from being fired
  22776. */
  22777. stopDetect: function stopDetect() {
  22778. // clone current data to the store as the previous gesture
  22779. // used for the double tap gesture, since this is an other gesture detect session
  22780. this.previous = Hammer.utils.extend({}, this.current);
  22781. // reset the current
  22782. this.current = null;
  22783. // stopped!
  22784. this.stopped = true;
  22785. },
  22786. /**
  22787. * extend eventData for Hammer.gestures
  22788. * @param {Object} ev
  22789. * @returns {Object} ev
  22790. */
  22791. extendEventData: function extendEventData(ev) {
  22792. var startEv = this.current.startEvent;
  22793. // if the touches change, set the new touches over the startEvent touches
  22794. // this because touchevents don't have all the touches on touchstart, or the
  22795. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  22796. // but, sometimes it happens that both fingers are touching at the EXACT same time
  22797. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  22798. // extend 1 level deep to get the touchlist with the touch objects
  22799. startEv.touches = [];
  22800. for(var i=0,len=ev.touches.length; i<len; i++) {
  22801. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  22802. }
  22803. }
  22804. var delta_time = ev.timeStamp - startEv.timeStamp,
  22805. delta_x = ev.center.pageX - startEv.center.pageX,
  22806. delta_y = ev.center.pageY - startEv.center.pageY,
  22807. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  22808. Hammer.utils.extend(ev, {
  22809. deltaTime : delta_time,
  22810. deltaX : delta_x,
  22811. deltaY : delta_y,
  22812. velocityX : velocity.x,
  22813. velocityY : velocity.y,
  22814. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  22815. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  22816. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  22817. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  22818. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  22819. startEvent : startEv
  22820. });
  22821. return ev;
  22822. },
  22823. /**
  22824. * register new gesture
  22825. * @param {Object} gesture object, see gestures.js for documentation
  22826. * @returns {Array} gestures
  22827. */
  22828. register: function register(gesture) {
  22829. // add an enable gesture options if there is no given
  22830. var options = gesture.defaults || {};
  22831. if(options[gesture.name] === undefined) {
  22832. options[gesture.name] = true;
  22833. }
  22834. // extend Hammer default options with the Hammer.gesture options
  22835. Hammer.utils.extend(Hammer.defaults, options, true);
  22836. // set its index
  22837. gesture.index = gesture.index || 1000;
  22838. // add Hammer.gesture to the list
  22839. this.gestures.push(gesture);
  22840. // sort the list by index
  22841. this.gestures.sort(function(a, b) {
  22842. if (a.index < b.index) {
  22843. return -1;
  22844. }
  22845. if (a.index > b.index) {
  22846. return 1;
  22847. }
  22848. return 0;
  22849. });
  22850. return this.gestures;
  22851. }
  22852. };
  22853. Hammer.gestures = Hammer.gestures || {};
  22854. /**
  22855. * Custom gestures
  22856. * ==============================
  22857. *
  22858. * Gesture object
  22859. * --------------------
  22860. * The object structure of a gesture:
  22861. *
  22862. * { name: 'mygesture',
  22863. * index: 1337,
  22864. * defaults: {
  22865. * mygesture_option: true
  22866. * }
  22867. * handler: function(type, ev, inst) {
  22868. * // trigger gesture event
  22869. * inst.trigger(this.name, ev);
  22870. * }
  22871. * }
  22872. * @param {String} name
  22873. * this should be the name of the gesture, lowercase
  22874. * it is also being used to disable/enable the gesture per instance config.
  22875. *
  22876. * @param {Number} [index=1000]
  22877. * the index of the gesture, where it is going to be in the stack of gestures detection
  22878. * like when you build an gesture that depends on the drag gesture, it is a good
  22879. * idea to place it after the index of the drag gesture.
  22880. *
  22881. * @param {Object} [defaults={}]
  22882. * the default settings of the gesture. these are added to the instance settings,
  22883. * and can be overruled per instance. you can also add the name of the gesture,
  22884. * but this is also added by default (and set to true).
  22885. *
  22886. * @param {Function} handler
  22887. * this handles the gesture detection of your custom gesture and receives the
  22888. * following arguments:
  22889. *
  22890. * @param {Object} eventData
  22891. * event data containing the following properties:
  22892. * timeStamp {Number} time the event occurred
  22893. * target {HTMLElement} target element
  22894. * touches {Array} touches (fingers, pointers, mouse) on the screen
  22895. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  22896. * center {Object} center position of the touches. contains pageX and pageY
  22897. * deltaTime {Number} the total time of the touches in the screen
  22898. * deltaX {Number} the delta on x axis we haved moved
  22899. * deltaY {Number} the delta on y axis we haved moved
  22900. * velocityX {Number} the velocity on the x
  22901. * velocityY {Number} the velocity on y
  22902. * angle {Number} the angle we are moving
  22903. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  22904. * distance {Number} the distance we haved moved
  22905. * scale {Number} scaling of the touches, needs 2 touches
  22906. * rotation {Number} rotation of the touches, needs 2 touches *
  22907. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  22908. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  22909. * startEvent {Object} contains the same properties as above,
  22910. * but from the first touch. this is used to calculate
  22911. * distances, deltaTime, scaling etc
  22912. *
  22913. * @param {Hammer.Instance} inst
  22914. * the instance we are doing the detection for. you can get the options from
  22915. * the inst.options object and trigger the gesture event by calling inst.trigger
  22916. *
  22917. *
  22918. * Handle gestures
  22919. * --------------------
  22920. * inside the handler you can get/set Hammer.detection.current. This is the current
  22921. * detection session. It has the following properties
  22922. * @param {String} name
  22923. * contains the name of the gesture we have detected. it has not a real function,
  22924. * only to check in other gestures if something is detected.
  22925. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  22926. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  22927. *
  22928. * @readonly
  22929. * @param {Hammer.Instance} inst
  22930. * the instance we do the detection for
  22931. *
  22932. * @readonly
  22933. * @param {Object} startEvent
  22934. * contains the properties of the first gesture detection in this session.
  22935. * Used for calculations about timing, distance, etc.
  22936. *
  22937. * @readonly
  22938. * @param {Object} lastEvent
  22939. * contains all the properties of the last gesture detect in this session.
  22940. *
  22941. * after the gesture detection session has been completed (user has released the screen)
  22942. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  22943. * this is usefull for gestures like doubletap, where you need to know if the
  22944. * previous gesture was a tap
  22945. *
  22946. * options that have been set by the instance can be received by calling inst.options
  22947. *
  22948. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  22949. * The first param is the name of your gesture, the second the event argument
  22950. *
  22951. *
  22952. * Register gestures
  22953. * --------------------
  22954. * When an gesture is added to the Hammer.gestures object, it is auto registered
  22955. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  22956. * manually and pass your gesture object as a param
  22957. *
  22958. */
  22959. /**
  22960. * Hold
  22961. * Touch stays at the same place for x time
  22962. * @events hold
  22963. */
  22964. Hammer.gestures.Hold = {
  22965. name: 'hold',
  22966. index: 10,
  22967. defaults: {
  22968. hold_timeout : 500,
  22969. hold_threshold : 1
  22970. },
  22971. timer: null,
  22972. handler: function holdGesture(ev, inst) {
  22973. switch(ev.eventType) {
  22974. case Hammer.EVENT_START:
  22975. // clear any running timers
  22976. clearTimeout(this.timer);
  22977. // set the gesture so we can check in the timeout if it still is
  22978. Hammer.detection.current.name = this.name;
  22979. // set timer and if after the timeout it still is hold,
  22980. // we trigger the hold event
  22981. this.timer = setTimeout(function() {
  22982. if(Hammer.detection.current.name == 'hold') {
  22983. inst.trigger('hold', ev);
  22984. }
  22985. }, inst.options.hold_timeout);
  22986. break;
  22987. // when you move or end we clear the timer
  22988. case Hammer.EVENT_MOVE:
  22989. if(ev.distance > inst.options.hold_threshold) {
  22990. clearTimeout(this.timer);
  22991. }
  22992. break;
  22993. case Hammer.EVENT_END:
  22994. clearTimeout(this.timer);
  22995. break;
  22996. }
  22997. }
  22998. };
  22999. /**
  23000. * Tap/DoubleTap
  23001. * Quick touch at a place or double at the same place
  23002. * @events tap, doubletap
  23003. */
  23004. Hammer.gestures.Tap = {
  23005. name: 'tap',
  23006. index: 100,
  23007. defaults: {
  23008. tap_max_touchtime : 250,
  23009. tap_max_distance : 10,
  23010. tap_always : true,
  23011. doubletap_distance : 20,
  23012. doubletap_interval : 300
  23013. },
  23014. handler: function tapGesture(ev, inst) {
  23015. if(ev.eventType == Hammer.EVENT_END) {
  23016. // previous gesture, for the double tap since these are two different gesture detections
  23017. var prev = Hammer.detection.previous,
  23018. did_doubletap = false;
  23019. // when the touchtime is higher then the max touch time
  23020. // or when the moving distance is too much
  23021. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  23022. ev.distance > inst.options.tap_max_distance) {
  23023. return;
  23024. }
  23025. // check if double tap
  23026. if(prev && prev.name == 'tap' &&
  23027. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  23028. ev.distance < inst.options.doubletap_distance) {
  23029. inst.trigger('doubletap', ev);
  23030. did_doubletap = true;
  23031. }
  23032. // do a single tap
  23033. if(!did_doubletap || inst.options.tap_always) {
  23034. Hammer.detection.current.name = 'tap';
  23035. inst.trigger(Hammer.detection.current.name, ev);
  23036. }
  23037. }
  23038. }
  23039. };
  23040. /**
  23041. * Swipe
  23042. * triggers swipe events when the end velocity is above the threshold
  23043. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  23044. */
  23045. Hammer.gestures.Swipe = {
  23046. name: 'swipe',
  23047. index: 40,
  23048. defaults: {
  23049. // set 0 for unlimited, but this can conflict with transform
  23050. swipe_max_touches : 1,
  23051. swipe_velocity : 0.7
  23052. },
  23053. handler: function swipeGesture(ev, inst) {
  23054. if(ev.eventType == Hammer.EVENT_END) {
  23055. // max touches
  23056. if(inst.options.swipe_max_touches > 0 &&
  23057. ev.touches.length > inst.options.swipe_max_touches) {
  23058. return;
  23059. }
  23060. // when the distance we moved is too small we skip this gesture
  23061. // or we can be already in dragging
  23062. if(ev.velocityX > inst.options.swipe_velocity ||
  23063. ev.velocityY > inst.options.swipe_velocity) {
  23064. // trigger swipe events
  23065. inst.trigger(this.name, ev);
  23066. inst.trigger(this.name + ev.direction, ev);
  23067. }
  23068. }
  23069. }
  23070. };
  23071. /**
  23072. * Drag
  23073. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  23074. * moving left and right is a good practice. When all the drag events are blocking
  23075. * you disable scrolling on that area.
  23076. * @events drag, drapleft, dragright, dragup, dragdown
  23077. */
  23078. Hammer.gestures.Drag = {
  23079. name: 'drag',
  23080. index: 50,
  23081. defaults: {
  23082. drag_min_distance : 10,
  23083. // set 0 for unlimited, but this can conflict with transform
  23084. drag_max_touches : 1,
  23085. // prevent default browser behavior when dragging occurs
  23086. // be careful with it, it makes the element a blocking element
  23087. // when you are using the drag gesture, it is a good practice to set this true
  23088. drag_block_horizontal : false,
  23089. drag_block_vertical : false,
  23090. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  23091. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  23092. drag_lock_to_axis : false,
  23093. // drag lock only kicks in when distance > drag_lock_min_distance
  23094. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  23095. drag_lock_min_distance : 25
  23096. },
  23097. triggered: false,
  23098. handler: function dragGesture(ev, inst) {
  23099. // current gesture isnt drag, but dragged is true
  23100. // this means an other gesture is busy. now call dragend
  23101. if(Hammer.detection.current.name != this.name && this.triggered) {
  23102. inst.trigger(this.name +'end', ev);
  23103. this.triggered = false;
  23104. return;
  23105. }
  23106. // max touches
  23107. if(inst.options.drag_max_touches > 0 &&
  23108. ev.touches.length > inst.options.drag_max_touches) {
  23109. return;
  23110. }
  23111. switch(ev.eventType) {
  23112. case Hammer.EVENT_START:
  23113. this.triggered = false;
  23114. break;
  23115. case Hammer.EVENT_MOVE:
  23116. // when the distance we moved is too small we skip this gesture
  23117. // or we can be already in dragging
  23118. if(ev.distance < inst.options.drag_min_distance &&
  23119. Hammer.detection.current.name != this.name) {
  23120. return;
  23121. }
  23122. // we are dragging!
  23123. Hammer.detection.current.name = this.name;
  23124. // lock drag to axis?
  23125. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  23126. ev.drag_locked_to_axis = true;
  23127. }
  23128. var last_direction = Hammer.detection.current.lastEvent.direction;
  23129. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  23130. // keep direction on the axis that the drag gesture started on
  23131. if(Hammer.utils.isVertical(last_direction)) {
  23132. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  23133. }
  23134. else {
  23135. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  23136. }
  23137. }
  23138. // first time, trigger dragstart event
  23139. if(!this.triggered) {
  23140. inst.trigger(this.name +'start', ev);
  23141. this.triggered = true;
  23142. }
  23143. // trigger normal event
  23144. inst.trigger(this.name, ev);
  23145. // direction event, like dragdown
  23146. inst.trigger(this.name + ev.direction, ev);
  23147. // block the browser events
  23148. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  23149. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  23150. ev.preventDefault();
  23151. }
  23152. break;
  23153. case Hammer.EVENT_END:
  23154. // trigger dragend
  23155. if(this.triggered) {
  23156. inst.trigger(this.name +'end', ev);
  23157. }
  23158. this.triggered = false;
  23159. break;
  23160. }
  23161. }
  23162. };
  23163. /**
  23164. * Transform
  23165. * User want to scale or rotate with 2 fingers
  23166. * @events transform, pinch, pinchin, pinchout, rotate
  23167. */
  23168. Hammer.gestures.Transform = {
  23169. name: 'transform',
  23170. index: 45,
  23171. defaults: {
  23172. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  23173. transform_min_scale : 0.01,
  23174. // rotation in degrees
  23175. transform_min_rotation : 1,
  23176. // prevent default browser behavior when two touches are on the screen
  23177. // but it makes the element a blocking element
  23178. // when you are using the transform gesture, it is a good practice to set this true
  23179. transform_always_block : false
  23180. },
  23181. triggered: false,
  23182. handler: function transformGesture(ev, inst) {
  23183. // current gesture isnt drag, but dragged is true
  23184. // this means an other gesture is busy. now call dragend
  23185. if(Hammer.detection.current.name != this.name && this.triggered) {
  23186. inst.trigger(this.name +'end', ev);
  23187. this.triggered = false;
  23188. return;
  23189. }
  23190. // atleast multitouch
  23191. if(ev.touches.length < 2) {
  23192. return;
  23193. }
  23194. // prevent default when two fingers are on the screen
  23195. if(inst.options.transform_always_block) {
  23196. ev.preventDefault();
  23197. }
  23198. switch(ev.eventType) {
  23199. case Hammer.EVENT_START:
  23200. this.triggered = false;
  23201. break;
  23202. case Hammer.EVENT_MOVE:
  23203. var scale_threshold = Math.abs(1-ev.scale);
  23204. var rotation_threshold = Math.abs(ev.rotation);
  23205. // when the distance we moved is too small we skip this gesture
  23206. // or we can be already in dragging
  23207. if(scale_threshold < inst.options.transform_min_scale &&
  23208. rotation_threshold < inst.options.transform_min_rotation) {
  23209. return;
  23210. }
  23211. // we are transforming!
  23212. Hammer.detection.current.name = this.name;
  23213. // first time, trigger dragstart event
  23214. if(!this.triggered) {
  23215. inst.trigger(this.name +'start', ev);
  23216. this.triggered = true;
  23217. }
  23218. inst.trigger(this.name, ev); // basic transform event
  23219. // trigger rotate event
  23220. if(rotation_threshold > inst.options.transform_min_rotation) {
  23221. inst.trigger('rotate', ev);
  23222. }
  23223. // trigger pinch event
  23224. if(scale_threshold > inst.options.transform_min_scale) {
  23225. inst.trigger('pinch', ev);
  23226. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  23227. }
  23228. break;
  23229. case Hammer.EVENT_END:
  23230. // trigger dragend
  23231. if(this.triggered) {
  23232. inst.trigger(this.name +'end', ev);
  23233. }
  23234. this.triggered = false;
  23235. break;
  23236. }
  23237. }
  23238. };
  23239. /**
  23240. * Touch
  23241. * Called as first, tells the user has touched the screen
  23242. * @events touch
  23243. */
  23244. Hammer.gestures.Touch = {
  23245. name: 'touch',
  23246. index: -Infinity,
  23247. defaults: {
  23248. // call preventDefault at touchstart, and makes the element blocking by
  23249. // disabling the scrolling of the page, but it improves gestures like
  23250. // transforming and dragging.
  23251. // be careful with using this, it can be very annoying for users to be stuck
  23252. // on the page
  23253. prevent_default: false,
  23254. // disable mouse events, so only touch (or pen!) input triggers events
  23255. prevent_mouseevents: false
  23256. },
  23257. handler: function touchGesture(ev, inst) {
  23258. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  23259. ev.stopDetect();
  23260. return;
  23261. }
  23262. if(inst.options.prevent_default) {
  23263. ev.preventDefault();
  23264. }
  23265. if(ev.eventType == Hammer.EVENT_START) {
  23266. inst.trigger(this.name, ev);
  23267. }
  23268. }
  23269. };
  23270. /**
  23271. * Release
  23272. * Called as last, tells the user has released the screen
  23273. * @events release
  23274. */
  23275. Hammer.gestures.Release = {
  23276. name: 'release',
  23277. index: Infinity,
  23278. handler: function releaseGesture(ev, inst) {
  23279. if(ev.eventType == Hammer.EVENT_END) {
  23280. inst.trigger(this.name, ev);
  23281. }
  23282. }
  23283. };
  23284. // node export
  23285. if(typeof module === 'object' && typeof module.exports === 'object'){
  23286. module.exports = Hammer;
  23287. }
  23288. // just window export
  23289. else {
  23290. window.Hammer = Hammer;
  23291. // requireJS module definition
  23292. if(typeof window.define === 'function' && window.define.amd) {
  23293. window.define('hammer', [], function() {
  23294. return Hammer;
  23295. });
  23296. }
  23297. }
  23298. })(this);
  23299. },{}],4:[function(require,module,exports){
  23300. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  23301. //! version : 2.7.0
  23302. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  23303. //! license : MIT
  23304. //! momentjs.com
  23305. (function (undefined) {
  23306. /************************************
  23307. Constants
  23308. ************************************/
  23309. var moment,
  23310. VERSION = "2.7.0",
  23311. // the global-scope this is NOT the global object in Node.js
  23312. globalScope = typeof global !== 'undefined' ? global : this,
  23313. oldGlobalMoment,
  23314. round = Math.round,
  23315. i,
  23316. YEAR = 0,
  23317. MONTH = 1,
  23318. DATE = 2,
  23319. HOUR = 3,
  23320. MINUTE = 4,
  23321. SECOND = 5,
  23322. MILLISECOND = 6,
  23323. // internal storage for language config files
  23324. languages = {},
  23325. // moment internal properties
  23326. momentProperties = {
  23327. _isAMomentObject: null,
  23328. _i : null,
  23329. _f : null,
  23330. _l : null,
  23331. _strict : null,
  23332. _tzm : null,
  23333. _isUTC : null,
  23334. _offset : null, // optional. Combine with _isUTC
  23335. _pf : null,
  23336. _lang : null // optional
  23337. },
  23338. // check for nodeJS
  23339. hasModule = (typeof module !== 'undefined' && module.exports),
  23340. // ASP.NET json date format regex
  23341. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  23342. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  23343. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  23344. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  23345. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  23346. // format tokens
  23347. 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,
  23348. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  23349. // parsing token regexes
  23350. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  23351. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  23352. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  23353. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  23354. parseTokenDigits = /\d+/, // nonzero number of digits
  23355. 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.
  23356. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  23357. parseTokenT = /T/i, // T (ISO separator)
  23358. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  23359. parseTokenOrdinal = /\d{1,2}/,
  23360. //strict parsing regexes
  23361. parseTokenOneDigit = /\d/, // 0 - 9
  23362. parseTokenTwoDigits = /\d\d/, // 00 - 99
  23363. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  23364. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  23365. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  23366. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  23367. // iso 8601 regex
  23368. // 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)
  23369. 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)?)?$/,
  23370. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  23371. isoDates = [
  23372. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  23373. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  23374. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  23375. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  23376. ['YYYY-DDD', /\d{4}-\d{3}/]
  23377. ],
  23378. // iso time formats and regexes
  23379. isoTimes = [
  23380. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  23381. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  23382. ['HH:mm', /(T| )\d\d:\d\d/],
  23383. ['HH', /(T| )\d\d/]
  23384. ],
  23385. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  23386. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  23387. // getter and setter names
  23388. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  23389. unitMillisecondFactors = {
  23390. 'Milliseconds' : 1,
  23391. 'Seconds' : 1e3,
  23392. 'Minutes' : 6e4,
  23393. 'Hours' : 36e5,
  23394. 'Days' : 864e5,
  23395. 'Months' : 2592e6,
  23396. 'Years' : 31536e6
  23397. },
  23398. unitAliases = {
  23399. ms : 'millisecond',
  23400. s : 'second',
  23401. m : 'minute',
  23402. h : 'hour',
  23403. d : 'day',
  23404. D : 'date',
  23405. w : 'week',
  23406. W : 'isoWeek',
  23407. M : 'month',
  23408. Q : 'quarter',
  23409. y : 'year',
  23410. DDD : 'dayOfYear',
  23411. e : 'weekday',
  23412. E : 'isoWeekday',
  23413. gg: 'weekYear',
  23414. GG: 'isoWeekYear'
  23415. },
  23416. camelFunctions = {
  23417. dayofyear : 'dayOfYear',
  23418. isoweekday : 'isoWeekday',
  23419. isoweek : 'isoWeek',
  23420. weekyear : 'weekYear',
  23421. isoweekyear : 'isoWeekYear'
  23422. },
  23423. // format function strings
  23424. formatFunctions = {},
  23425. // default relative time thresholds
  23426. relativeTimeThresholds = {
  23427. s: 45, //seconds to minutes
  23428. m: 45, //minutes to hours
  23429. h: 22, //hours to days
  23430. dd: 25, //days to month (month == 1)
  23431. dm: 45, //days to months (months > 1)
  23432. dy: 345 //days to year
  23433. },
  23434. // tokens to ordinalize and pad
  23435. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  23436. paddedTokens = 'M D H h m s w W'.split(' '),
  23437. formatTokenFunctions = {
  23438. M : function () {
  23439. return this.month() + 1;
  23440. },
  23441. MMM : function (format) {
  23442. return this.lang().monthsShort(this, format);
  23443. },
  23444. MMMM : function (format) {
  23445. return this.lang().months(this, format);
  23446. },
  23447. D : function () {
  23448. return this.date();
  23449. },
  23450. DDD : function () {
  23451. return this.dayOfYear();
  23452. },
  23453. d : function () {
  23454. return this.day();
  23455. },
  23456. dd : function (format) {
  23457. return this.lang().weekdaysMin(this, format);
  23458. },
  23459. ddd : function (format) {
  23460. return this.lang().weekdaysShort(this, format);
  23461. },
  23462. dddd : function (format) {
  23463. return this.lang().weekdays(this, format);
  23464. },
  23465. w : function () {
  23466. return this.week();
  23467. },
  23468. W : function () {
  23469. return this.isoWeek();
  23470. },
  23471. YY : function () {
  23472. return leftZeroFill(this.year() % 100, 2);
  23473. },
  23474. YYYY : function () {
  23475. return leftZeroFill(this.year(), 4);
  23476. },
  23477. YYYYY : function () {
  23478. return leftZeroFill(this.year(), 5);
  23479. },
  23480. YYYYYY : function () {
  23481. var y = this.year(), sign = y >= 0 ? '+' : '-';
  23482. return sign + leftZeroFill(Math.abs(y), 6);
  23483. },
  23484. gg : function () {
  23485. return leftZeroFill(this.weekYear() % 100, 2);
  23486. },
  23487. gggg : function () {
  23488. return leftZeroFill(this.weekYear(), 4);
  23489. },
  23490. ggggg : function () {
  23491. return leftZeroFill(this.weekYear(), 5);
  23492. },
  23493. GG : function () {
  23494. return leftZeroFill(this.isoWeekYear() % 100, 2);
  23495. },
  23496. GGGG : function () {
  23497. return leftZeroFill(this.isoWeekYear(), 4);
  23498. },
  23499. GGGGG : function () {
  23500. return leftZeroFill(this.isoWeekYear(), 5);
  23501. },
  23502. e : function () {
  23503. return this.weekday();
  23504. },
  23505. E : function () {
  23506. return this.isoWeekday();
  23507. },
  23508. a : function () {
  23509. return this.lang().meridiem(this.hours(), this.minutes(), true);
  23510. },
  23511. A : function () {
  23512. return this.lang().meridiem(this.hours(), this.minutes(), false);
  23513. },
  23514. H : function () {
  23515. return this.hours();
  23516. },
  23517. h : function () {
  23518. return this.hours() % 12 || 12;
  23519. },
  23520. m : function () {
  23521. return this.minutes();
  23522. },
  23523. s : function () {
  23524. return this.seconds();
  23525. },
  23526. S : function () {
  23527. return toInt(this.milliseconds() / 100);
  23528. },
  23529. SS : function () {
  23530. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  23531. },
  23532. SSS : function () {
  23533. return leftZeroFill(this.milliseconds(), 3);
  23534. },
  23535. SSSS : function () {
  23536. return leftZeroFill(this.milliseconds(), 3);
  23537. },
  23538. Z : function () {
  23539. var a = -this.zone(),
  23540. b = "+";
  23541. if (a < 0) {
  23542. a = -a;
  23543. b = "-";
  23544. }
  23545. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  23546. },
  23547. ZZ : function () {
  23548. var a = -this.zone(),
  23549. b = "+";
  23550. if (a < 0) {
  23551. a = -a;
  23552. b = "-";
  23553. }
  23554. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  23555. },
  23556. z : function () {
  23557. return this.zoneAbbr();
  23558. },
  23559. zz : function () {
  23560. return this.zoneName();
  23561. },
  23562. X : function () {
  23563. return this.unix();
  23564. },
  23565. Q : function () {
  23566. return this.quarter();
  23567. }
  23568. },
  23569. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  23570. // Pick the first defined of two or three arguments. dfl comes from
  23571. // default.
  23572. function dfl(a, b, c) {
  23573. switch (arguments.length) {
  23574. case 2: return a != null ? a : b;
  23575. case 3: return a != null ? a : b != null ? b : c;
  23576. default: throw new Error("Implement me");
  23577. }
  23578. }
  23579. function defaultParsingFlags() {
  23580. // We need to deep clone this object, and es5 standard is not very
  23581. // helpful.
  23582. return {
  23583. empty : false,
  23584. unusedTokens : [],
  23585. unusedInput : [],
  23586. overflow : -2,
  23587. charsLeftOver : 0,
  23588. nullInput : false,
  23589. invalidMonth : null,
  23590. invalidFormat : false,
  23591. userInvalidated : false,
  23592. iso: false
  23593. };
  23594. }
  23595. function deprecate(msg, fn) {
  23596. var firstTime = true;
  23597. function printMsg() {
  23598. if (moment.suppressDeprecationWarnings === false &&
  23599. typeof console !== 'undefined' && console.warn) {
  23600. console.warn("Deprecation warning: " + msg);
  23601. }
  23602. }
  23603. return extend(function () {
  23604. if (firstTime) {
  23605. printMsg();
  23606. firstTime = false;
  23607. }
  23608. return fn.apply(this, arguments);
  23609. }, fn);
  23610. }
  23611. function padToken(func, count) {
  23612. return function (a) {
  23613. return leftZeroFill(func.call(this, a), count);
  23614. };
  23615. }
  23616. function ordinalizeToken(func, period) {
  23617. return function (a) {
  23618. return this.lang().ordinal(func.call(this, a), period);
  23619. };
  23620. }
  23621. while (ordinalizeTokens.length) {
  23622. i = ordinalizeTokens.pop();
  23623. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  23624. }
  23625. while (paddedTokens.length) {
  23626. i = paddedTokens.pop();
  23627. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  23628. }
  23629. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  23630. /************************************
  23631. Constructors
  23632. ************************************/
  23633. function Language() {
  23634. }
  23635. // Moment prototype object
  23636. function Moment(config) {
  23637. checkOverflow(config);
  23638. extend(this, config);
  23639. }
  23640. // Duration Constructor
  23641. function Duration(duration) {
  23642. var normalizedInput = normalizeObjectUnits(duration),
  23643. years = normalizedInput.year || 0,
  23644. quarters = normalizedInput.quarter || 0,
  23645. months = normalizedInput.month || 0,
  23646. weeks = normalizedInput.week || 0,
  23647. days = normalizedInput.day || 0,
  23648. hours = normalizedInput.hour || 0,
  23649. minutes = normalizedInput.minute || 0,
  23650. seconds = normalizedInput.second || 0,
  23651. milliseconds = normalizedInput.millisecond || 0;
  23652. // representation for dateAddRemove
  23653. this._milliseconds = +milliseconds +
  23654. seconds * 1e3 + // 1000
  23655. minutes * 6e4 + // 1000 * 60
  23656. hours * 36e5; // 1000 * 60 * 60
  23657. // Because of dateAddRemove treats 24 hours as different from a
  23658. // day when working around DST, we need to store them separately
  23659. this._days = +days +
  23660. weeks * 7;
  23661. // It is impossible translate months into days without knowing
  23662. // which months you are are talking about, so we have to store
  23663. // it separately.
  23664. this._months = +months +
  23665. quarters * 3 +
  23666. years * 12;
  23667. this._data = {};
  23668. this._bubble();
  23669. }
  23670. /************************************
  23671. Helpers
  23672. ************************************/
  23673. function extend(a, b) {
  23674. for (var i in b) {
  23675. if (b.hasOwnProperty(i)) {
  23676. a[i] = b[i];
  23677. }
  23678. }
  23679. if (b.hasOwnProperty("toString")) {
  23680. a.toString = b.toString;
  23681. }
  23682. if (b.hasOwnProperty("valueOf")) {
  23683. a.valueOf = b.valueOf;
  23684. }
  23685. return a;
  23686. }
  23687. function cloneMoment(m) {
  23688. var result = {}, i;
  23689. for (i in m) {
  23690. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  23691. result[i] = m[i];
  23692. }
  23693. }
  23694. return result;
  23695. }
  23696. function absRound(number) {
  23697. if (number < 0) {
  23698. return Math.ceil(number);
  23699. } else {
  23700. return Math.floor(number);
  23701. }
  23702. }
  23703. // left zero fill a number
  23704. // see http://jsperf.com/left-zero-filling for performance comparison
  23705. function leftZeroFill(number, targetLength, forceSign) {
  23706. var output = '' + Math.abs(number),
  23707. sign = number >= 0;
  23708. while (output.length < targetLength) {
  23709. output = '0' + output;
  23710. }
  23711. return (sign ? (forceSign ? '+' : '') : '-') + output;
  23712. }
  23713. // helper function for _.addTime and _.subtractTime
  23714. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  23715. var milliseconds = duration._milliseconds,
  23716. days = duration._days,
  23717. months = duration._months;
  23718. updateOffset = updateOffset == null ? true : updateOffset;
  23719. if (milliseconds) {
  23720. mom._d.setTime(+mom._d + milliseconds * isAdding);
  23721. }
  23722. if (days) {
  23723. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  23724. }
  23725. if (months) {
  23726. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  23727. }
  23728. if (updateOffset) {
  23729. moment.updateOffset(mom, days || months);
  23730. }
  23731. }
  23732. // check if is an array
  23733. function isArray(input) {
  23734. return Object.prototype.toString.call(input) === '[object Array]';
  23735. }
  23736. function isDate(input) {
  23737. return Object.prototype.toString.call(input) === '[object Date]' ||
  23738. input instanceof Date;
  23739. }
  23740. // compare two arrays, return the number of differences
  23741. function compareArrays(array1, array2, dontConvert) {
  23742. var len = Math.min(array1.length, array2.length),
  23743. lengthDiff = Math.abs(array1.length - array2.length),
  23744. diffs = 0,
  23745. i;
  23746. for (i = 0; i < len; i++) {
  23747. if ((dontConvert && array1[i] !== array2[i]) ||
  23748. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  23749. diffs++;
  23750. }
  23751. }
  23752. return diffs + lengthDiff;
  23753. }
  23754. function normalizeUnits(units) {
  23755. if (units) {
  23756. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  23757. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  23758. }
  23759. return units;
  23760. }
  23761. function normalizeObjectUnits(inputObject) {
  23762. var normalizedInput = {},
  23763. normalizedProp,
  23764. prop;
  23765. for (prop in inputObject) {
  23766. if (inputObject.hasOwnProperty(prop)) {
  23767. normalizedProp = normalizeUnits(prop);
  23768. if (normalizedProp) {
  23769. normalizedInput[normalizedProp] = inputObject[prop];
  23770. }
  23771. }
  23772. }
  23773. return normalizedInput;
  23774. }
  23775. function makeList(field) {
  23776. var count, setter;
  23777. if (field.indexOf('week') === 0) {
  23778. count = 7;
  23779. setter = 'day';
  23780. }
  23781. else if (field.indexOf('month') === 0) {
  23782. count = 12;
  23783. setter = 'month';
  23784. }
  23785. else {
  23786. return;
  23787. }
  23788. moment[field] = function (format, index) {
  23789. var i, getter,
  23790. method = moment.fn._lang[field],
  23791. results = [];
  23792. if (typeof format === 'number') {
  23793. index = format;
  23794. format = undefined;
  23795. }
  23796. getter = function (i) {
  23797. var m = moment().utc().set(setter, i);
  23798. return method.call(moment.fn._lang, m, format || '');
  23799. };
  23800. if (index != null) {
  23801. return getter(index);
  23802. }
  23803. else {
  23804. for (i = 0; i < count; i++) {
  23805. results.push(getter(i));
  23806. }
  23807. return results;
  23808. }
  23809. };
  23810. }
  23811. function toInt(argumentForCoercion) {
  23812. var coercedNumber = +argumentForCoercion,
  23813. value = 0;
  23814. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  23815. if (coercedNumber >= 0) {
  23816. value = Math.floor(coercedNumber);
  23817. } else {
  23818. value = Math.ceil(coercedNumber);
  23819. }
  23820. }
  23821. return value;
  23822. }
  23823. function daysInMonth(year, month) {
  23824. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  23825. }
  23826. function weeksInYear(year, dow, doy) {
  23827. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  23828. }
  23829. function daysInYear(year) {
  23830. return isLeapYear(year) ? 366 : 365;
  23831. }
  23832. function isLeapYear(year) {
  23833. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  23834. }
  23835. function checkOverflow(m) {
  23836. var overflow;
  23837. if (m._a && m._pf.overflow === -2) {
  23838. overflow =
  23839. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  23840. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  23841. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  23842. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  23843. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  23844. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  23845. -1;
  23846. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  23847. overflow = DATE;
  23848. }
  23849. m._pf.overflow = overflow;
  23850. }
  23851. }
  23852. function isValid(m) {
  23853. if (m._isValid == null) {
  23854. m._isValid = !isNaN(m._d.getTime()) &&
  23855. m._pf.overflow < 0 &&
  23856. !m._pf.empty &&
  23857. !m._pf.invalidMonth &&
  23858. !m._pf.nullInput &&
  23859. !m._pf.invalidFormat &&
  23860. !m._pf.userInvalidated;
  23861. if (m._strict) {
  23862. m._isValid = m._isValid &&
  23863. m._pf.charsLeftOver === 0 &&
  23864. m._pf.unusedTokens.length === 0;
  23865. }
  23866. }
  23867. return m._isValid;
  23868. }
  23869. function normalizeLanguage(key) {
  23870. return key ? key.toLowerCase().replace('_', '-') : key;
  23871. }
  23872. // Return a moment from input, that is local/utc/zone equivalent to model.
  23873. function makeAs(input, model) {
  23874. return model._isUTC ? moment(input).zone(model._offset || 0) :
  23875. moment(input).local();
  23876. }
  23877. /************************************
  23878. Languages
  23879. ************************************/
  23880. extend(Language.prototype, {
  23881. set : function (config) {
  23882. var prop, i;
  23883. for (i in config) {
  23884. prop = config[i];
  23885. if (typeof prop === 'function') {
  23886. this[i] = prop;
  23887. } else {
  23888. this['_' + i] = prop;
  23889. }
  23890. }
  23891. },
  23892. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  23893. months : function (m) {
  23894. return this._months[m.month()];
  23895. },
  23896. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  23897. monthsShort : function (m) {
  23898. return this._monthsShort[m.month()];
  23899. },
  23900. monthsParse : function (monthName) {
  23901. var i, mom, regex;
  23902. if (!this._monthsParse) {
  23903. this._monthsParse = [];
  23904. }
  23905. for (i = 0; i < 12; i++) {
  23906. // make the regex if we don't have it already
  23907. if (!this._monthsParse[i]) {
  23908. mom = moment.utc([2000, i]);
  23909. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  23910. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  23911. }
  23912. // test the regex
  23913. if (this._monthsParse[i].test(monthName)) {
  23914. return i;
  23915. }
  23916. }
  23917. },
  23918. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  23919. weekdays : function (m) {
  23920. return this._weekdays[m.day()];
  23921. },
  23922. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  23923. weekdaysShort : function (m) {
  23924. return this._weekdaysShort[m.day()];
  23925. },
  23926. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  23927. weekdaysMin : function (m) {
  23928. return this._weekdaysMin[m.day()];
  23929. },
  23930. weekdaysParse : function (weekdayName) {
  23931. var i, mom, regex;
  23932. if (!this._weekdaysParse) {
  23933. this._weekdaysParse = [];
  23934. }
  23935. for (i = 0; i < 7; i++) {
  23936. // make the regex if we don't have it already
  23937. if (!this._weekdaysParse[i]) {
  23938. mom = moment([2000, 1]).day(i);
  23939. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  23940. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  23941. }
  23942. // test the regex
  23943. if (this._weekdaysParse[i].test(weekdayName)) {
  23944. return i;
  23945. }
  23946. }
  23947. },
  23948. _longDateFormat : {
  23949. LT : "h:mm A",
  23950. L : "MM/DD/YYYY",
  23951. LL : "MMMM D YYYY",
  23952. LLL : "MMMM D YYYY LT",
  23953. LLLL : "dddd, MMMM D YYYY LT"
  23954. },
  23955. longDateFormat : function (key) {
  23956. var output = this._longDateFormat[key];
  23957. if (!output && this._longDateFormat[key.toUpperCase()]) {
  23958. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  23959. return val.slice(1);
  23960. });
  23961. this._longDateFormat[key] = output;
  23962. }
  23963. return output;
  23964. },
  23965. isPM : function (input) {
  23966. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  23967. // Using charAt should be more compatible.
  23968. return ((input + '').toLowerCase().charAt(0) === 'p');
  23969. },
  23970. _meridiemParse : /[ap]\.?m?\.?/i,
  23971. meridiem : function (hours, minutes, isLower) {
  23972. if (hours > 11) {
  23973. return isLower ? 'pm' : 'PM';
  23974. } else {
  23975. return isLower ? 'am' : 'AM';
  23976. }
  23977. },
  23978. _calendar : {
  23979. sameDay : '[Today at] LT',
  23980. nextDay : '[Tomorrow at] LT',
  23981. nextWeek : 'dddd [at] LT',
  23982. lastDay : '[Yesterday at] LT',
  23983. lastWeek : '[Last] dddd [at] LT',
  23984. sameElse : 'L'
  23985. },
  23986. calendar : function (key, mom) {
  23987. var output = this._calendar[key];
  23988. return typeof output === 'function' ? output.apply(mom) : output;
  23989. },
  23990. _relativeTime : {
  23991. future : "in %s",
  23992. past : "%s ago",
  23993. s : "a few seconds",
  23994. m : "a minute",
  23995. mm : "%d minutes",
  23996. h : "an hour",
  23997. hh : "%d hours",
  23998. d : "a day",
  23999. dd : "%d days",
  24000. M : "a month",
  24001. MM : "%d months",
  24002. y : "a year",
  24003. yy : "%d years"
  24004. },
  24005. relativeTime : function (number, withoutSuffix, string, isFuture) {
  24006. var output = this._relativeTime[string];
  24007. return (typeof output === 'function') ?
  24008. output(number, withoutSuffix, string, isFuture) :
  24009. output.replace(/%d/i, number);
  24010. },
  24011. pastFuture : function (diff, output) {
  24012. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  24013. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  24014. },
  24015. ordinal : function (number) {
  24016. return this._ordinal.replace("%d", number);
  24017. },
  24018. _ordinal : "%d",
  24019. preparse : function (string) {
  24020. return string;
  24021. },
  24022. postformat : function (string) {
  24023. return string;
  24024. },
  24025. week : function (mom) {
  24026. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  24027. },
  24028. _week : {
  24029. dow : 0, // Sunday is the first day of the week.
  24030. doy : 6 // The week that contains Jan 1st is the first week of the year.
  24031. },
  24032. _invalidDate: 'Invalid date',
  24033. invalidDate: function () {
  24034. return this._invalidDate;
  24035. }
  24036. });
  24037. // Loads a language definition into the `languages` cache. The function
  24038. // takes a key and optionally values. If not in the browser and no values
  24039. // are provided, it will load the language file module. As a convenience,
  24040. // this function also returns the language values.
  24041. function loadLang(key, values) {
  24042. values.abbr = key;
  24043. if (!languages[key]) {
  24044. languages[key] = new Language();
  24045. }
  24046. languages[key].set(values);
  24047. return languages[key];
  24048. }
  24049. // Remove a language from the `languages` cache. Mostly useful in tests.
  24050. function unloadLang(key) {
  24051. delete languages[key];
  24052. }
  24053. // Determines which language definition to use and returns it.
  24054. //
  24055. // With no parameters, it will return the global language. If you
  24056. // pass in a language key, such as 'en', it will return the
  24057. // definition for 'en', so long as 'en' has already been loaded using
  24058. // moment.lang.
  24059. function getLangDefinition(key) {
  24060. var i = 0, j, lang, next, split,
  24061. get = function (k) {
  24062. if (!languages[k] && hasModule) {
  24063. try {
  24064. require('./lang/' + k);
  24065. } catch (e) { }
  24066. }
  24067. return languages[k];
  24068. };
  24069. if (!key) {
  24070. return moment.fn._lang;
  24071. }
  24072. if (!isArray(key)) {
  24073. //short-circuit everything else
  24074. lang = get(key);
  24075. if (lang) {
  24076. return lang;
  24077. }
  24078. key = [key];
  24079. }
  24080. //pick the language from the array
  24081. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  24082. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  24083. while (i < key.length) {
  24084. split = normalizeLanguage(key[i]).split('-');
  24085. j = split.length;
  24086. next = normalizeLanguage(key[i + 1]);
  24087. next = next ? next.split('-') : null;
  24088. while (j > 0) {
  24089. lang = get(split.slice(0, j).join('-'));
  24090. if (lang) {
  24091. return lang;
  24092. }
  24093. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  24094. //the next array item is better than a shallower substring of this one
  24095. break;
  24096. }
  24097. j--;
  24098. }
  24099. i++;
  24100. }
  24101. return moment.fn._lang;
  24102. }
  24103. /************************************
  24104. Formatting
  24105. ************************************/
  24106. function removeFormattingTokens(input) {
  24107. if (input.match(/\[[\s\S]/)) {
  24108. return input.replace(/^\[|\]$/g, "");
  24109. }
  24110. return input.replace(/\\/g, "");
  24111. }
  24112. function makeFormatFunction(format) {
  24113. var array = format.match(formattingTokens), i, length;
  24114. for (i = 0, length = array.length; i < length; i++) {
  24115. if (formatTokenFunctions[array[i]]) {
  24116. array[i] = formatTokenFunctions[array[i]];
  24117. } else {
  24118. array[i] = removeFormattingTokens(array[i]);
  24119. }
  24120. }
  24121. return function (mom) {
  24122. var output = "";
  24123. for (i = 0; i < length; i++) {
  24124. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  24125. }
  24126. return output;
  24127. };
  24128. }
  24129. // format date using native date object
  24130. function formatMoment(m, format) {
  24131. if (!m.isValid()) {
  24132. return m.lang().invalidDate();
  24133. }
  24134. format = expandFormat(format, m.lang());
  24135. if (!formatFunctions[format]) {
  24136. formatFunctions[format] = makeFormatFunction(format);
  24137. }
  24138. return formatFunctions[format](m);
  24139. }
  24140. function expandFormat(format, lang) {
  24141. var i = 5;
  24142. function replaceLongDateFormatTokens(input) {
  24143. return lang.longDateFormat(input) || input;
  24144. }
  24145. localFormattingTokens.lastIndex = 0;
  24146. while (i >= 0 && localFormattingTokens.test(format)) {
  24147. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  24148. localFormattingTokens.lastIndex = 0;
  24149. i -= 1;
  24150. }
  24151. return format;
  24152. }
  24153. /************************************
  24154. Parsing
  24155. ************************************/
  24156. // get the regex to find the next token
  24157. function getParseRegexForToken(token, config) {
  24158. var a, strict = config._strict;
  24159. switch (token) {
  24160. case 'Q':
  24161. return parseTokenOneDigit;
  24162. case 'DDDD':
  24163. return parseTokenThreeDigits;
  24164. case 'YYYY':
  24165. case 'GGGG':
  24166. case 'gggg':
  24167. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  24168. case 'Y':
  24169. case 'G':
  24170. case 'g':
  24171. return parseTokenSignedNumber;
  24172. case 'YYYYYY':
  24173. case 'YYYYY':
  24174. case 'GGGGG':
  24175. case 'ggggg':
  24176. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  24177. case 'S':
  24178. if (strict) { return parseTokenOneDigit; }
  24179. /* falls through */
  24180. case 'SS':
  24181. if (strict) { return parseTokenTwoDigits; }
  24182. /* falls through */
  24183. case 'SSS':
  24184. if (strict) { return parseTokenThreeDigits; }
  24185. /* falls through */
  24186. case 'DDD':
  24187. return parseTokenOneToThreeDigits;
  24188. case 'MMM':
  24189. case 'MMMM':
  24190. case 'dd':
  24191. case 'ddd':
  24192. case 'dddd':
  24193. return parseTokenWord;
  24194. case 'a':
  24195. case 'A':
  24196. return getLangDefinition(config._l)._meridiemParse;
  24197. case 'X':
  24198. return parseTokenTimestampMs;
  24199. case 'Z':
  24200. case 'ZZ':
  24201. return parseTokenTimezone;
  24202. case 'T':
  24203. return parseTokenT;
  24204. case 'SSSS':
  24205. return parseTokenDigits;
  24206. case 'MM':
  24207. case 'DD':
  24208. case 'YY':
  24209. case 'GG':
  24210. case 'gg':
  24211. case 'HH':
  24212. case 'hh':
  24213. case 'mm':
  24214. case 'ss':
  24215. case 'ww':
  24216. case 'WW':
  24217. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  24218. case 'M':
  24219. case 'D':
  24220. case 'd':
  24221. case 'H':
  24222. case 'h':
  24223. case 'm':
  24224. case 's':
  24225. case 'w':
  24226. case 'W':
  24227. case 'e':
  24228. case 'E':
  24229. return parseTokenOneOrTwoDigits;
  24230. case 'Do':
  24231. return parseTokenOrdinal;
  24232. default :
  24233. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  24234. return a;
  24235. }
  24236. }
  24237. function timezoneMinutesFromString(string) {
  24238. string = string || "";
  24239. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  24240. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  24241. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  24242. minutes = +(parts[1] * 60) + toInt(parts[2]);
  24243. return parts[0] === '+' ? -minutes : minutes;
  24244. }
  24245. // function to convert string input to date
  24246. function addTimeToArrayFromToken(token, input, config) {
  24247. var a, datePartArray = config._a;
  24248. switch (token) {
  24249. // QUARTER
  24250. case 'Q':
  24251. if (input != null) {
  24252. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  24253. }
  24254. break;
  24255. // MONTH
  24256. case 'M' : // fall through to MM
  24257. case 'MM' :
  24258. if (input != null) {
  24259. datePartArray[MONTH] = toInt(input) - 1;
  24260. }
  24261. break;
  24262. case 'MMM' : // fall through to MMMM
  24263. case 'MMMM' :
  24264. a = getLangDefinition(config._l).monthsParse(input);
  24265. // if we didn't find a month name, mark the date as invalid.
  24266. if (a != null) {
  24267. datePartArray[MONTH] = a;
  24268. } else {
  24269. config._pf.invalidMonth = input;
  24270. }
  24271. break;
  24272. // DAY OF MONTH
  24273. case 'D' : // fall through to DD
  24274. case 'DD' :
  24275. if (input != null) {
  24276. datePartArray[DATE] = toInt(input);
  24277. }
  24278. break;
  24279. case 'Do' :
  24280. if (input != null) {
  24281. datePartArray[DATE] = toInt(parseInt(input, 10));
  24282. }
  24283. break;
  24284. // DAY OF YEAR
  24285. case 'DDD' : // fall through to DDDD
  24286. case 'DDDD' :
  24287. if (input != null) {
  24288. config._dayOfYear = toInt(input);
  24289. }
  24290. break;
  24291. // YEAR
  24292. case 'YY' :
  24293. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  24294. break;
  24295. case 'YYYY' :
  24296. case 'YYYYY' :
  24297. case 'YYYYYY' :
  24298. datePartArray[YEAR] = toInt(input);
  24299. break;
  24300. // AM / PM
  24301. case 'a' : // fall through to A
  24302. case 'A' :
  24303. config._isPm = getLangDefinition(config._l).isPM(input);
  24304. break;
  24305. // 24 HOUR
  24306. case 'H' : // fall through to hh
  24307. case 'HH' : // fall through to hh
  24308. case 'h' : // fall through to hh
  24309. case 'hh' :
  24310. datePartArray[HOUR] = toInt(input);
  24311. break;
  24312. // MINUTE
  24313. case 'm' : // fall through to mm
  24314. case 'mm' :
  24315. datePartArray[MINUTE] = toInt(input);
  24316. break;
  24317. // SECOND
  24318. case 's' : // fall through to ss
  24319. case 'ss' :
  24320. datePartArray[SECOND] = toInt(input);
  24321. break;
  24322. // MILLISECOND
  24323. case 'S' :
  24324. case 'SS' :
  24325. case 'SSS' :
  24326. case 'SSSS' :
  24327. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  24328. break;
  24329. // UNIX TIMESTAMP WITH MS
  24330. case 'X':
  24331. config._d = new Date(parseFloat(input) * 1000);
  24332. break;
  24333. // TIMEZONE
  24334. case 'Z' : // fall through to ZZ
  24335. case 'ZZ' :
  24336. config._useUTC = true;
  24337. config._tzm = timezoneMinutesFromString(input);
  24338. break;
  24339. // WEEKDAY - human
  24340. case 'dd':
  24341. case 'ddd':
  24342. case 'dddd':
  24343. a = getLangDefinition(config._l).weekdaysParse(input);
  24344. // if we didn't get a weekday name, mark the date as invalid
  24345. if (a != null) {
  24346. config._w = config._w || {};
  24347. config._w['d'] = a;
  24348. } else {
  24349. config._pf.invalidWeekday = input;
  24350. }
  24351. break;
  24352. // WEEK, WEEK DAY - numeric
  24353. case 'w':
  24354. case 'ww':
  24355. case 'W':
  24356. case 'WW':
  24357. case 'd':
  24358. case 'e':
  24359. case 'E':
  24360. token = token.substr(0, 1);
  24361. /* falls through */
  24362. case 'gggg':
  24363. case 'GGGG':
  24364. case 'GGGGG':
  24365. token = token.substr(0, 2);
  24366. if (input) {
  24367. config._w = config._w || {};
  24368. config._w[token] = toInt(input);
  24369. }
  24370. break;
  24371. case 'gg':
  24372. case 'GG':
  24373. config._w = config._w || {};
  24374. config._w[token] = moment.parseTwoDigitYear(input);
  24375. }
  24376. }
  24377. function dayOfYearFromWeekInfo(config) {
  24378. var w, weekYear, week, weekday, dow, doy, temp, lang;
  24379. w = config._w;
  24380. if (w.GG != null || w.W != null || w.E != null) {
  24381. dow = 1;
  24382. doy = 4;
  24383. // TODO: We need to take the current isoWeekYear, but that depends on
  24384. // how we interpret now (local, utc, fixed offset). So create
  24385. // a now version of current config (take local/utc/offset flags, and
  24386. // create now).
  24387. weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
  24388. week = dfl(w.W, 1);
  24389. weekday = dfl(w.E, 1);
  24390. } else {
  24391. lang = getLangDefinition(config._l);
  24392. dow = lang._week.dow;
  24393. doy = lang._week.doy;
  24394. weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
  24395. week = dfl(w.w, 1);
  24396. if (w.d != null) {
  24397. // weekday -- low day numbers are considered next week
  24398. weekday = w.d;
  24399. if (weekday < dow) {
  24400. ++week;
  24401. }
  24402. } else if (w.e != null) {
  24403. // local weekday -- counting starts from begining of week
  24404. weekday = w.e + dow;
  24405. } else {
  24406. // default to begining of week
  24407. weekday = dow;
  24408. }
  24409. }
  24410. temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
  24411. config._a[YEAR] = temp.year;
  24412. config._dayOfYear = temp.dayOfYear;
  24413. }
  24414. // convert an array to a date.
  24415. // the array should mirror the parameters below
  24416. // note: all values past the year are optional and will default to the lowest possible value.
  24417. // [year, month, day , hour, minute, second, millisecond]
  24418. function dateFromConfig(config) {
  24419. var i, date, input = [], currentDate, yearToUse;
  24420. if (config._d) {
  24421. return;
  24422. }
  24423. currentDate = currentDateArray(config);
  24424. //compute day of the year from weeks and weekdays
  24425. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  24426. dayOfYearFromWeekInfo(config);
  24427. }
  24428. //if the day of the year is set, figure out what it is
  24429. if (config._dayOfYear) {
  24430. yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
  24431. if (config._dayOfYear > daysInYear(yearToUse)) {
  24432. config._pf._overflowDayOfYear = true;
  24433. }
  24434. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  24435. config._a[MONTH] = date.getUTCMonth();
  24436. config._a[DATE] = date.getUTCDate();
  24437. }
  24438. // Default to current date.
  24439. // * if no year, month, day of month are given, default to today
  24440. // * if day of month is given, default month and year
  24441. // * if month is given, default only year
  24442. // * if year is given, don't default anything
  24443. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  24444. config._a[i] = input[i] = currentDate[i];
  24445. }
  24446. // Zero out whatever was not defaulted, including time
  24447. for (; i < 7; i++) {
  24448. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  24449. }
  24450. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  24451. // Apply timezone offset from input. The actual zone can be changed
  24452. // with parseZone.
  24453. if (config._tzm != null) {
  24454. config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
  24455. }
  24456. }
  24457. function dateFromObject(config) {
  24458. var normalizedInput;
  24459. if (config._d) {
  24460. return;
  24461. }
  24462. normalizedInput = normalizeObjectUnits(config._i);
  24463. config._a = [
  24464. normalizedInput.year,
  24465. normalizedInput.month,
  24466. normalizedInput.day,
  24467. normalizedInput.hour,
  24468. normalizedInput.minute,
  24469. normalizedInput.second,
  24470. normalizedInput.millisecond
  24471. ];
  24472. dateFromConfig(config);
  24473. }
  24474. function currentDateArray(config) {
  24475. var now = new Date();
  24476. if (config._useUTC) {
  24477. return [
  24478. now.getUTCFullYear(),
  24479. now.getUTCMonth(),
  24480. now.getUTCDate()
  24481. ];
  24482. } else {
  24483. return [now.getFullYear(), now.getMonth(), now.getDate()];
  24484. }
  24485. }
  24486. // date from string and format string
  24487. function makeDateFromStringAndFormat(config) {
  24488. if (config._f === moment.ISO_8601) {
  24489. parseISO(config);
  24490. return;
  24491. }
  24492. config._a = [];
  24493. config._pf.empty = true;
  24494. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  24495. var lang = getLangDefinition(config._l),
  24496. string = '' + config._i,
  24497. i, parsedInput, tokens, token, skipped,
  24498. stringLength = string.length,
  24499. totalParsedInputLength = 0;
  24500. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  24501. for (i = 0; i < tokens.length; i++) {
  24502. token = tokens[i];
  24503. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  24504. if (parsedInput) {
  24505. skipped = string.substr(0, string.indexOf(parsedInput));
  24506. if (skipped.length > 0) {
  24507. config._pf.unusedInput.push(skipped);
  24508. }
  24509. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  24510. totalParsedInputLength += parsedInput.length;
  24511. }
  24512. // don't parse if it's not a known token
  24513. if (formatTokenFunctions[token]) {
  24514. if (parsedInput) {
  24515. config._pf.empty = false;
  24516. }
  24517. else {
  24518. config._pf.unusedTokens.push(token);
  24519. }
  24520. addTimeToArrayFromToken(token, parsedInput, config);
  24521. }
  24522. else if (config._strict && !parsedInput) {
  24523. config._pf.unusedTokens.push(token);
  24524. }
  24525. }
  24526. // add remaining unparsed input length to the string
  24527. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  24528. if (string.length > 0) {
  24529. config._pf.unusedInput.push(string);
  24530. }
  24531. // handle am pm
  24532. if (config._isPm && config._a[HOUR] < 12) {
  24533. config._a[HOUR] += 12;
  24534. }
  24535. // if is 12 am, change hours to 0
  24536. if (config._isPm === false && config._a[HOUR] === 12) {
  24537. config._a[HOUR] = 0;
  24538. }
  24539. dateFromConfig(config);
  24540. checkOverflow(config);
  24541. }
  24542. function unescapeFormat(s) {
  24543. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  24544. return p1 || p2 || p3 || p4;
  24545. });
  24546. }
  24547. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  24548. function regexpEscape(s) {
  24549. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  24550. }
  24551. // date from string and array of format strings
  24552. function makeDateFromStringAndArray(config) {
  24553. var tempConfig,
  24554. bestMoment,
  24555. scoreToBeat,
  24556. i,
  24557. currentScore;
  24558. if (config._f.length === 0) {
  24559. config._pf.invalidFormat = true;
  24560. config._d = new Date(NaN);
  24561. return;
  24562. }
  24563. for (i = 0; i < config._f.length; i++) {
  24564. currentScore = 0;
  24565. tempConfig = extend({}, config);
  24566. tempConfig._pf = defaultParsingFlags();
  24567. tempConfig._f = config._f[i];
  24568. makeDateFromStringAndFormat(tempConfig);
  24569. if (!isValid(tempConfig)) {
  24570. continue;
  24571. }
  24572. // if there is any input that was not parsed add a penalty for that format
  24573. currentScore += tempConfig._pf.charsLeftOver;
  24574. //or tokens
  24575. currentScore += tempConfig._pf.unusedTokens.length * 10;
  24576. tempConfig._pf.score = currentScore;
  24577. if (scoreToBeat == null || currentScore < scoreToBeat) {
  24578. scoreToBeat = currentScore;
  24579. bestMoment = tempConfig;
  24580. }
  24581. }
  24582. extend(config, bestMoment || tempConfig);
  24583. }
  24584. // date from iso format
  24585. function parseISO(config) {
  24586. var i, l,
  24587. string = config._i,
  24588. match = isoRegex.exec(string);
  24589. if (match) {
  24590. config._pf.iso = true;
  24591. for (i = 0, l = isoDates.length; i < l; i++) {
  24592. if (isoDates[i][1].exec(string)) {
  24593. // match[5] should be "T" or undefined
  24594. config._f = isoDates[i][0] + (match[6] || " ");
  24595. break;
  24596. }
  24597. }
  24598. for (i = 0, l = isoTimes.length; i < l; i++) {
  24599. if (isoTimes[i][1].exec(string)) {
  24600. config._f += isoTimes[i][0];
  24601. break;
  24602. }
  24603. }
  24604. if (string.match(parseTokenTimezone)) {
  24605. config._f += "Z";
  24606. }
  24607. makeDateFromStringAndFormat(config);
  24608. } else {
  24609. config._isValid = false;
  24610. }
  24611. }
  24612. // date from iso format or fallback
  24613. function makeDateFromString(config) {
  24614. parseISO(config);
  24615. if (config._isValid === false) {
  24616. delete config._isValid;
  24617. moment.createFromInputFallback(config);
  24618. }
  24619. }
  24620. function makeDateFromInput(config) {
  24621. var input = config._i,
  24622. matched = aspNetJsonRegex.exec(input);
  24623. if (input === undefined) {
  24624. config._d = new Date();
  24625. } else if (matched) {
  24626. config._d = new Date(+matched[1]);
  24627. } else if (typeof input === 'string') {
  24628. makeDateFromString(config);
  24629. } else if (isArray(input)) {
  24630. config._a = input.slice(0);
  24631. dateFromConfig(config);
  24632. } else if (isDate(input)) {
  24633. config._d = new Date(+input);
  24634. } else if (typeof(input) === 'object') {
  24635. dateFromObject(config);
  24636. } else if (typeof(input) === 'number') {
  24637. // from milliseconds
  24638. config._d = new Date(input);
  24639. } else {
  24640. moment.createFromInputFallback(config);
  24641. }
  24642. }
  24643. function makeDate(y, m, d, h, M, s, ms) {
  24644. //can't just apply() to create a date:
  24645. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  24646. var date = new Date(y, m, d, h, M, s, ms);
  24647. //the date constructor doesn't accept years < 1970
  24648. if (y < 1970) {
  24649. date.setFullYear(y);
  24650. }
  24651. return date;
  24652. }
  24653. function makeUTCDate(y) {
  24654. var date = new Date(Date.UTC.apply(null, arguments));
  24655. if (y < 1970) {
  24656. date.setUTCFullYear(y);
  24657. }
  24658. return date;
  24659. }
  24660. function parseWeekday(input, language) {
  24661. if (typeof input === 'string') {
  24662. if (!isNaN(input)) {
  24663. input = parseInt(input, 10);
  24664. }
  24665. else {
  24666. input = language.weekdaysParse(input);
  24667. if (typeof input !== 'number') {
  24668. return null;
  24669. }
  24670. }
  24671. }
  24672. return input;
  24673. }
  24674. /************************************
  24675. Relative Time
  24676. ************************************/
  24677. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  24678. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  24679. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  24680. }
  24681. function relativeTime(milliseconds, withoutSuffix, lang) {
  24682. var seconds = round(Math.abs(milliseconds) / 1000),
  24683. minutes = round(seconds / 60),
  24684. hours = round(minutes / 60),
  24685. days = round(hours / 24),
  24686. years = round(days / 365),
  24687. args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
  24688. minutes === 1 && ['m'] ||
  24689. minutes < relativeTimeThresholds.m && ['mm', minutes] ||
  24690. hours === 1 && ['h'] ||
  24691. hours < relativeTimeThresholds.h && ['hh', hours] ||
  24692. days === 1 && ['d'] ||
  24693. days <= relativeTimeThresholds.dd && ['dd', days] ||
  24694. days <= relativeTimeThresholds.dm && ['M'] ||
  24695. days < relativeTimeThresholds.dy && ['MM', round(days / 30)] ||
  24696. years === 1 && ['y'] || ['yy', years];
  24697. args[2] = withoutSuffix;
  24698. args[3] = milliseconds > 0;
  24699. args[4] = lang;
  24700. return substituteTimeAgo.apply({}, args);
  24701. }
  24702. /************************************
  24703. Week of Year
  24704. ************************************/
  24705. // firstDayOfWeek 0 = sun, 6 = sat
  24706. // the day of the week that starts the week
  24707. // (usually sunday or monday)
  24708. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  24709. // the first week is the week that contains the first
  24710. // of this day of the week
  24711. // (eg. ISO weeks use thursday (4))
  24712. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  24713. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  24714. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  24715. adjustedMoment;
  24716. if (daysToDayOfWeek > end) {
  24717. daysToDayOfWeek -= 7;
  24718. }
  24719. if (daysToDayOfWeek < end - 7) {
  24720. daysToDayOfWeek += 7;
  24721. }
  24722. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  24723. return {
  24724. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  24725. year: adjustedMoment.year()
  24726. };
  24727. }
  24728. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  24729. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  24730. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  24731. d = d === 0 ? 7 : d;
  24732. weekday = weekday != null ? weekday : firstDayOfWeek;
  24733. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  24734. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  24735. return {
  24736. year: dayOfYear > 0 ? year : year - 1,
  24737. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  24738. };
  24739. }
  24740. /************************************
  24741. Top Level Functions
  24742. ************************************/
  24743. function makeMoment(config) {
  24744. var input = config._i,
  24745. format = config._f;
  24746. if (input === null || (format === undefined && input === '')) {
  24747. return moment.invalid({nullInput: true});
  24748. }
  24749. if (typeof input === 'string') {
  24750. config._i = input = getLangDefinition().preparse(input);
  24751. }
  24752. if (moment.isMoment(input)) {
  24753. config = cloneMoment(input);
  24754. config._d = new Date(+input._d);
  24755. } else if (format) {
  24756. if (isArray(format)) {
  24757. makeDateFromStringAndArray(config);
  24758. } else {
  24759. makeDateFromStringAndFormat(config);
  24760. }
  24761. } else {
  24762. makeDateFromInput(config);
  24763. }
  24764. return new Moment(config);
  24765. }
  24766. moment = function (input, format, lang, strict) {
  24767. var c;
  24768. if (typeof(lang) === "boolean") {
  24769. strict = lang;
  24770. lang = undefined;
  24771. }
  24772. // object construction must be done this way.
  24773. // https://github.com/moment/moment/issues/1423
  24774. c = {};
  24775. c._isAMomentObject = true;
  24776. c._i = input;
  24777. c._f = format;
  24778. c._l = lang;
  24779. c._strict = strict;
  24780. c._isUTC = false;
  24781. c._pf = defaultParsingFlags();
  24782. return makeMoment(c);
  24783. };
  24784. moment.suppressDeprecationWarnings = false;
  24785. moment.createFromInputFallback = deprecate(
  24786. "moment construction falls back to js Date. This is " +
  24787. "discouraged and will be removed in upcoming major " +
  24788. "release. Please refer to " +
  24789. "https://github.com/moment/moment/issues/1407 for more info.",
  24790. function (config) {
  24791. config._d = new Date(config._i);
  24792. });
  24793. // Pick a moment m from moments so that m[fn](other) is true for all
  24794. // other. This relies on the function fn to be transitive.
  24795. //
  24796. // moments should either be an array of moment objects or an array, whose
  24797. // first element is an array of moment objects.
  24798. function pickBy(fn, moments) {
  24799. var res, i;
  24800. if (moments.length === 1 && isArray(moments[0])) {
  24801. moments = moments[0];
  24802. }
  24803. if (!moments.length) {
  24804. return moment();
  24805. }
  24806. res = moments[0];
  24807. for (i = 1; i < moments.length; ++i) {
  24808. if (moments[i][fn](res)) {
  24809. res = moments[i];
  24810. }
  24811. }
  24812. return res;
  24813. }
  24814. moment.min = function () {
  24815. var args = [].slice.call(arguments, 0);
  24816. return pickBy('isBefore', args);
  24817. };
  24818. moment.max = function () {
  24819. var args = [].slice.call(arguments, 0);
  24820. return pickBy('isAfter', args);
  24821. };
  24822. // creating with utc
  24823. moment.utc = function (input, format, lang, strict) {
  24824. var c;
  24825. if (typeof(lang) === "boolean") {
  24826. strict = lang;
  24827. lang = undefined;
  24828. }
  24829. // object construction must be done this way.
  24830. // https://github.com/moment/moment/issues/1423
  24831. c = {};
  24832. c._isAMomentObject = true;
  24833. c._useUTC = true;
  24834. c._isUTC = true;
  24835. c._l = lang;
  24836. c._i = input;
  24837. c._f = format;
  24838. c._strict = strict;
  24839. c._pf = defaultParsingFlags();
  24840. return makeMoment(c).utc();
  24841. };
  24842. // creating with unix timestamp (in seconds)
  24843. moment.unix = function (input) {
  24844. return moment(input * 1000);
  24845. };
  24846. // duration
  24847. moment.duration = function (input, key) {
  24848. var duration = input,
  24849. // matching against regexp is expensive, do it on demand
  24850. match = null,
  24851. sign,
  24852. ret,
  24853. parseIso;
  24854. if (moment.isDuration(input)) {
  24855. duration = {
  24856. ms: input._milliseconds,
  24857. d: input._days,
  24858. M: input._months
  24859. };
  24860. } else if (typeof input === 'number') {
  24861. duration = {};
  24862. if (key) {
  24863. duration[key] = input;
  24864. } else {
  24865. duration.milliseconds = input;
  24866. }
  24867. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  24868. sign = (match[1] === "-") ? -1 : 1;
  24869. duration = {
  24870. y: 0,
  24871. d: toInt(match[DATE]) * sign,
  24872. h: toInt(match[HOUR]) * sign,
  24873. m: toInt(match[MINUTE]) * sign,
  24874. s: toInt(match[SECOND]) * sign,
  24875. ms: toInt(match[MILLISECOND]) * sign
  24876. };
  24877. } else if (!!(match = isoDurationRegex.exec(input))) {
  24878. sign = (match[1] === "-") ? -1 : 1;
  24879. parseIso = function (inp) {
  24880. // We'd normally use ~~inp for this, but unfortunately it also
  24881. // converts floats to ints.
  24882. // inp may be undefined, so careful calling replace on it.
  24883. var res = inp && parseFloat(inp.replace(',', '.'));
  24884. // apply sign while we're at it
  24885. return (isNaN(res) ? 0 : res) * sign;
  24886. };
  24887. duration = {
  24888. y: parseIso(match[2]),
  24889. M: parseIso(match[3]),
  24890. d: parseIso(match[4]),
  24891. h: parseIso(match[5]),
  24892. m: parseIso(match[6]),
  24893. s: parseIso(match[7]),
  24894. w: parseIso(match[8])
  24895. };
  24896. }
  24897. ret = new Duration(duration);
  24898. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  24899. ret._lang = input._lang;
  24900. }
  24901. return ret;
  24902. };
  24903. // version number
  24904. moment.version = VERSION;
  24905. // default format
  24906. moment.defaultFormat = isoFormat;
  24907. // constant that refers to the ISO standard
  24908. moment.ISO_8601 = function () {};
  24909. // Plugins that add properties should also add the key here (null value),
  24910. // so we can properly clone ourselves.
  24911. moment.momentProperties = momentProperties;
  24912. // This function will be called whenever a moment is mutated.
  24913. // It is intended to keep the offset in sync with the timezone.
  24914. moment.updateOffset = function () {};
  24915. // This function allows you to set a threshold for relative time strings
  24916. moment.relativeTimeThreshold = function(threshold, limit) {
  24917. if (relativeTimeThresholds[threshold] === undefined) {
  24918. return false;
  24919. }
  24920. relativeTimeThresholds[threshold] = limit;
  24921. return true;
  24922. };
  24923. // This function will load languages and then set the global language. If
  24924. // no arguments are passed in, it will simply return the current global
  24925. // language key.
  24926. moment.lang = function (key, values) {
  24927. var r;
  24928. if (!key) {
  24929. return moment.fn._lang._abbr;
  24930. }
  24931. if (values) {
  24932. loadLang(normalizeLanguage(key), values);
  24933. } else if (values === null) {
  24934. unloadLang(key);
  24935. key = 'en';
  24936. } else if (!languages[key]) {
  24937. getLangDefinition(key);
  24938. }
  24939. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  24940. return r._abbr;
  24941. };
  24942. // returns language data
  24943. moment.langData = function (key) {
  24944. if (key && key._lang && key._lang._abbr) {
  24945. key = key._lang._abbr;
  24946. }
  24947. return getLangDefinition(key);
  24948. };
  24949. // compare moment object
  24950. moment.isMoment = function (obj) {
  24951. return obj instanceof Moment ||
  24952. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  24953. };
  24954. // for typechecking Duration objects
  24955. moment.isDuration = function (obj) {
  24956. return obj instanceof Duration;
  24957. };
  24958. for (i = lists.length - 1; i >= 0; --i) {
  24959. makeList(lists[i]);
  24960. }
  24961. moment.normalizeUnits = function (units) {
  24962. return normalizeUnits(units);
  24963. };
  24964. moment.invalid = function (flags) {
  24965. var m = moment.utc(NaN);
  24966. if (flags != null) {
  24967. extend(m._pf, flags);
  24968. }
  24969. else {
  24970. m._pf.userInvalidated = true;
  24971. }
  24972. return m;
  24973. };
  24974. moment.parseZone = function () {
  24975. return moment.apply(null, arguments).parseZone();
  24976. };
  24977. moment.parseTwoDigitYear = function (input) {
  24978. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  24979. };
  24980. /************************************
  24981. Moment Prototype
  24982. ************************************/
  24983. extend(moment.fn = Moment.prototype, {
  24984. clone : function () {
  24985. return moment(this);
  24986. },
  24987. valueOf : function () {
  24988. return +this._d + ((this._offset || 0) * 60000);
  24989. },
  24990. unix : function () {
  24991. return Math.floor(+this / 1000);
  24992. },
  24993. toString : function () {
  24994. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  24995. },
  24996. toDate : function () {
  24997. return this._offset ? new Date(+this) : this._d;
  24998. },
  24999. toISOString : function () {
  25000. var m = moment(this).utc();
  25001. if (0 < m.year() && m.year() <= 9999) {
  25002. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  25003. } else {
  25004. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  25005. }
  25006. },
  25007. toArray : function () {
  25008. var m = this;
  25009. return [
  25010. m.year(),
  25011. m.month(),
  25012. m.date(),
  25013. m.hours(),
  25014. m.minutes(),
  25015. m.seconds(),
  25016. m.milliseconds()
  25017. ];
  25018. },
  25019. isValid : function () {
  25020. return isValid(this);
  25021. },
  25022. isDSTShifted : function () {
  25023. if (this._a) {
  25024. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  25025. }
  25026. return false;
  25027. },
  25028. parsingFlags : function () {
  25029. return extend({}, this._pf);
  25030. },
  25031. invalidAt: function () {
  25032. return this._pf.overflow;
  25033. },
  25034. utc : function () {
  25035. return this.zone(0);
  25036. },
  25037. local : function () {
  25038. this.zone(0);
  25039. this._isUTC = false;
  25040. return this;
  25041. },
  25042. format : function (inputString) {
  25043. var output = formatMoment(this, inputString || moment.defaultFormat);
  25044. return this.lang().postformat(output);
  25045. },
  25046. add : function (input, val) {
  25047. var dur;
  25048. // switch args to support add('s', 1) and add(1, 's')
  25049. if (typeof input === 'string' && typeof val === 'string') {
  25050. dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
  25051. } else if (typeof input === 'string') {
  25052. dur = moment.duration(+val, input);
  25053. } else {
  25054. dur = moment.duration(input, val);
  25055. }
  25056. addOrSubtractDurationFromMoment(this, dur, 1);
  25057. return this;
  25058. },
  25059. subtract : function (input, val) {
  25060. var dur;
  25061. // switch args to support subtract('s', 1) and subtract(1, 's')
  25062. if (typeof input === 'string' && typeof val === 'string') {
  25063. dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
  25064. } else if (typeof input === 'string') {
  25065. dur = moment.duration(+val, input);
  25066. } else {
  25067. dur = moment.duration(input, val);
  25068. }
  25069. addOrSubtractDurationFromMoment(this, dur, -1);
  25070. return this;
  25071. },
  25072. diff : function (input, units, asFloat) {
  25073. var that = makeAs(input, this),
  25074. zoneDiff = (this.zone() - that.zone()) * 6e4,
  25075. diff, output;
  25076. units = normalizeUnits(units);
  25077. if (units === 'year' || units === 'month') {
  25078. // average number of days in the months in the given dates
  25079. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  25080. // difference in months
  25081. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  25082. // adjust by taking difference in days, average number of days
  25083. // and dst in the given months.
  25084. output += ((this - moment(this).startOf('month')) -
  25085. (that - moment(that).startOf('month'))) / diff;
  25086. // same as above but with zones, to negate all dst
  25087. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  25088. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  25089. if (units === 'year') {
  25090. output = output / 12;
  25091. }
  25092. } else {
  25093. diff = (this - that);
  25094. output = units === 'second' ? diff / 1e3 : // 1000
  25095. units === 'minute' ? diff / 6e4 : // 1000 * 60
  25096. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  25097. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  25098. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  25099. diff;
  25100. }
  25101. return asFloat ? output : absRound(output);
  25102. },
  25103. from : function (time, withoutSuffix) {
  25104. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  25105. },
  25106. fromNow : function (withoutSuffix) {
  25107. return this.from(moment(), withoutSuffix);
  25108. },
  25109. calendar : function (time) {
  25110. // We want to compare the start of today, vs this.
  25111. // Getting start-of-today depends on whether we're zone'd or not.
  25112. var now = time || moment(),
  25113. sod = makeAs(now, this).startOf('day'),
  25114. diff = this.diff(sod, 'days', true),
  25115. format = diff < -6 ? 'sameElse' :
  25116. diff < -1 ? 'lastWeek' :
  25117. diff < 0 ? 'lastDay' :
  25118. diff < 1 ? 'sameDay' :
  25119. diff < 2 ? 'nextDay' :
  25120. diff < 7 ? 'nextWeek' : 'sameElse';
  25121. return this.format(this.lang().calendar(format, this));
  25122. },
  25123. isLeapYear : function () {
  25124. return isLeapYear(this.year());
  25125. },
  25126. isDST : function () {
  25127. return (this.zone() < this.clone().month(0).zone() ||
  25128. this.zone() < this.clone().month(5).zone());
  25129. },
  25130. day : function (input) {
  25131. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  25132. if (input != null) {
  25133. input = parseWeekday(input, this.lang());
  25134. return this.add({ d : input - day });
  25135. } else {
  25136. return day;
  25137. }
  25138. },
  25139. month : makeAccessor('Month', true),
  25140. startOf: function (units) {
  25141. units = normalizeUnits(units);
  25142. // the following switch intentionally omits break keywords
  25143. // to utilize falling through the cases.
  25144. switch (units) {
  25145. case 'year':
  25146. this.month(0);
  25147. /* falls through */
  25148. case 'quarter':
  25149. case 'month':
  25150. this.date(1);
  25151. /* falls through */
  25152. case 'week':
  25153. case 'isoWeek':
  25154. case 'day':
  25155. this.hours(0);
  25156. /* falls through */
  25157. case 'hour':
  25158. this.minutes(0);
  25159. /* falls through */
  25160. case 'minute':
  25161. this.seconds(0);
  25162. /* falls through */
  25163. case 'second':
  25164. this.milliseconds(0);
  25165. /* falls through */
  25166. }
  25167. // weeks are a special case
  25168. if (units === 'week') {
  25169. this.weekday(0);
  25170. } else if (units === 'isoWeek') {
  25171. this.isoWeekday(1);
  25172. }
  25173. // quarters are also special
  25174. if (units === 'quarter') {
  25175. this.month(Math.floor(this.month() / 3) * 3);
  25176. }
  25177. return this;
  25178. },
  25179. endOf: function (units) {
  25180. units = normalizeUnits(units);
  25181. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  25182. },
  25183. isAfter: function (input, units) {
  25184. units = typeof units !== 'undefined' ? units : 'millisecond';
  25185. return +this.clone().startOf(units) > +moment(input).startOf(units);
  25186. },
  25187. isBefore: function (input, units) {
  25188. units = typeof units !== 'undefined' ? units : 'millisecond';
  25189. return +this.clone().startOf(units) < +moment(input).startOf(units);
  25190. },
  25191. isSame: function (input, units) {
  25192. units = units || 'ms';
  25193. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  25194. },
  25195. min: deprecate(
  25196. "moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",
  25197. function (other) {
  25198. other = moment.apply(null, arguments);
  25199. return other < this ? this : other;
  25200. }
  25201. ),
  25202. max: deprecate(
  25203. "moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",
  25204. function (other) {
  25205. other = moment.apply(null, arguments);
  25206. return other > this ? this : other;
  25207. }
  25208. ),
  25209. // keepTime = true means only change the timezone, without affecting
  25210. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  25211. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  25212. // adjust the time as needed, to be valid.
  25213. //
  25214. // Keeping the time actually adds/subtracts (one hour)
  25215. // from the actual represented time. That is why we call updateOffset
  25216. // a second time. In case it wants us to change the offset again
  25217. // _changeInProgress == true case, then we have to adjust, because
  25218. // there is no such time in the given timezone.
  25219. zone : function (input, keepTime) {
  25220. var offset = this._offset || 0;
  25221. if (input != null) {
  25222. if (typeof input === "string") {
  25223. input = timezoneMinutesFromString(input);
  25224. }
  25225. if (Math.abs(input) < 16) {
  25226. input = input * 60;
  25227. }
  25228. this._offset = input;
  25229. this._isUTC = true;
  25230. if (offset !== input) {
  25231. if (!keepTime || this._changeInProgress) {
  25232. addOrSubtractDurationFromMoment(this,
  25233. moment.duration(offset - input, 'm'), 1, false);
  25234. } else if (!this._changeInProgress) {
  25235. this._changeInProgress = true;
  25236. moment.updateOffset(this, true);
  25237. this._changeInProgress = null;
  25238. }
  25239. }
  25240. } else {
  25241. return this._isUTC ? offset : this._d.getTimezoneOffset();
  25242. }
  25243. return this;
  25244. },
  25245. zoneAbbr : function () {
  25246. return this._isUTC ? "UTC" : "";
  25247. },
  25248. zoneName : function () {
  25249. return this._isUTC ? "Coordinated Universal Time" : "";
  25250. },
  25251. parseZone : function () {
  25252. if (this._tzm) {
  25253. this.zone(this._tzm);
  25254. } else if (typeof this._i === 'string') {
  25255. this.zone(this._i);
  25256. }
  25257. return this;
  25258. },
  25259. hasAlignedHourOffset : function (input) {
  25260. if (!input) {
  25261. input = 0;
  25262. }
  25263. else {
  25264. input = moment(input).zone();
  25265. }
  25266. return (this.zone() - input) % 60 === 0;
  25267. },
  25268. daysInMonth : function () {
  25269. return daysInMonth(this.year(), this.month());
  25270. },
  25271. dayOfYear : function (input) {
  25272. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  25273. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  25274. },
  25275. quarter : function (input) {
  25276. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  25277. },
  25278. weekYear : function (input) {
  25279. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  25280. return input == null ? year : this.add("y", (input - year));
  25281. },
  25282. isoWeekYear : function (input) {
  25283. var year = weekOfYear(this, 1, 4).year;
  25284. return input == null ? year : this.add("y", (input - year));
  25285. },
  25286. week : function (input) {
  25287. var week = this.lang().week(this);
  25288. return input == null ? week : this.add("d", (input - week) * 7);
  25289. },
  25290. isoWeek : function (input) {
  25291. var week = weekOfYear(this, 1, 4).week;
  25292. return input == null ? week : this.add("d", (input - week) * 7);
  25293. },
  25294. weekday : function (input) {
  25295. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  25296. return input == null ? weekday : this.add("d", input - weekday);
  25297. },
  25298. isoWeekday : function (input) {
  25299. // behaves the same as moment#day except
  25300. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  25301. // as a setter, sunday should belong to the previous week.
  25302. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  25303. },
  25304. isoWeeksInYear : function () {
  25305. return weeksInYear(this.year(), 1, 4);
  25306. },
  25307. weeksInYear : function () {
  25308. var weekInfo = this._lang._week;
  25309. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  25310. },
  25311. get : function (units) {
  25312. units = normalizeUnits(units);
  25313. return this[units]();
  25314. },
  25315. set : function (units, value) {
  25316. units = normalizeUnits(units);
  25317. if (typeof this[units] === 'function') {
  25318. this[units](value);
  25319. }
  25320. return this;
  25321. },
  25322. // If passed a language key, it will set the language for this
  25323. // instance. Otherwise, it will return the language configuration
  25324. // variables for this instance.
  25325. lang : function (key) {
  25326. if (key === undefined) {
  25327. return this._lang;
  25328. } else {
  25329. this._lang = getLangDefinition(key);
  25330. return this;
  25331. }
  25332. }
  25333. });
  25334. function rawMonthSetter(mom, value) {
  25335. var dayOfMonth;
  25336. // TODO: Move this out of here!
  25337. if (typeof value === 'string') {
  25338. value = mom.lang().monthsParse(value);
  25339. // TODO: Another silent failure?
  25340. if (typeof value !== 'number') {
  25341. return mom;
  25342. }
  25343. }
  25344. dayOfMonth = Math.min(mom.date(),
  25345. daysInMonth(mom.year(), value));
  25346. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  25347. return mom;
  25348. }
  25349. function rawGetter(mom, unit) {
  25350. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  25351. }
  25352. function rawSetter(mom, unit, value) {
  25353. if (unit === 'Month') {
  25354. return rawMonthSetter(mom, value);
  25355. } else {
  25356. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  25357. }
  25358. }
  25359. function makeAccessor(unit, keepTime) {
  25360. return function (value) {
  25361. if (value != null) {
  25362. rawSetter(this, unit, value);
  25363. moment.updateOffset(this, keepTime);
  25364. return this;
  25365. } else {
  25366. return rawGetter(this, unit);
  25367. }
  25368. };
  25369. }
  25370. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  25371. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  25372. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  25373. // Setting the hour should keep the time, because the user explicitly
  25374. // specified which hour he wants. So trying to maintain the same hour (in
  25375. // a new timezone) makes sense. Adding/subtracting hours does not follow
  25376. // this rule.
  25377. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  25378. // moment.fn.month is defined separately
  25379. moment.fn.date = makeAccessor('Date', true);
  25380. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  25381. moment.fn.year = makeAccessor('FullYear', true);
  25382. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  25383. // add plural methods
  25384. moment.fn.days = moment.fn.day;
  25385. moment.fn.months = moment.fn.month;
  25386. moment.fn.weeks = moment.fn.week;
  25387. moment.fn.isoWeeks = moment.fn.isoWeek;
  25388. moment.fn.quarters = moment.fn.quarter;
  25389. // add aliased format methods
  25390. moment.fn.toJSON = moment.fn.toISOString;
  25391. /************************************
  25392. Duration Prototype
  25393. ************************************/
  25394. extend(moment.duration.fn = Duration.prototype, {
  25395. _bubble : function () {
  25396. var milliseconds = this._milliseconds,
  25397. days = this._days,
  25398. months = this._months,
  25399. data = this._data,
  25400. seconds, minutes, hours, years;
  25401. // The following code bubbles up values, see the tests for
  25402. // examples of what that means.
  25403. data.milliseconds = milliseconds % 1000;
  25404. seconds = absRound(milliseconds / 1000);
  25405. data.seconds = seconds % 60;
  25406. minutes = absRound(seconds / 60);
  25407. data.minutes = minutes % 60;
  25408. hours = absRound(minutes / 60);
  25409. data.hours = hours % 24;
  25410. days += absRound(hours / 24);
  25411. data.days = days % 30;
  25412. months += absRound(days / 30);
  25413. data.months = months % 12;
  25414. years = absRound(months / 12);
  25415. data.years = years;
  25416. },
  25417. weeks : function () {
  25418. return absRound(this.days() / 7);
  25419. },
  25420. valueOf : function () {
  25421. return this._milliseconds +
  25422. this._days * 864e5 +
  25423. (this._months % 12) * 2592e6 +
  25424. toInt(this._months / 12) * 31536e6;
  25425. },
  25426. humanize : function (withSuffix) {
  25427. var difference = +this,
  25428. output = relativeTime(difference, !withSuffix, this.lang());
  25429. if (withSuffix) {
  25430. output = this.lang().pastFuture(difference, output);
  25431. }
  25432. return this.lang().postformat(output);
  25433. },
  25434. add : function (input, val) {
  25435. // supports only 2.0-style add(1, 's') or add(moment)
  25436. var dur = moment.duration(input, val);
  25437. this._milliseconds += dur._milliseconds;
  25438. this._days += dur._days;
  25439. this._months += dur._months;
  25440. this._bubble();
  25441. return this;
  25442. },
  25443. subtract : function (input, val) {
  25444. var dur = moment.duration(input, val);
  25445. this._milliseconds -= dur._milliseconds;
  25446. this._days -= dur._days;
  25447. this._months -= dur._months;
  25448. this._bubble();
  25449. return this;
  25450. },
  25451. get : function (units) {
  25452. units = normalizeUnits(units);
  25453. return this[units.toLowerCase() + 's']();
  25454. },
  25455. as : function (units) {
  25456. units = normalizeUnits(units);
  25457. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  25458. },
  25459. lang : moment.fn.lang,
  25460. toIsoString : function () {
  25461. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  25462. var years = Math.abs(this.years()),
  25463. months = Math.abs(this.months()),
  25464. days = Math.abs(this.days()),
  25465. hours = Math.abs(this.hours()),
  25466. minutes = Math.abs(this.minutes()),
  25467. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  25468. if (!this.asSeconds()) {
  25469. // this is the same as C#'s (Noda) and python (isodate)...
  25470. // but not other JS (goog.date)
  25471. return 'P0D';
  25472. }
  25473. return (this.asSeconds() < 0 ? '-' : '') +
  25474. 'P' +
  25475. (years ? years + 'Y' : '') +
  25476. (months ? months + 'M' : '') +
  25477. (days ? days + 'D' : '') +
  25478. ((hours || minutes || seconds) ? 'T' : '') +
  25479. (hours ? hours + 'H' : '') +
  25480. (minutes ? minutes + 'M' : '') +
  25481. (seconds ? seconds + 'S' : '');
  25482. }
  25483. });
  25484. function makeDurationGetter(name) {
  25485. moment.duration.fn[name] = function () {
  25486. return this._data[name];
  25487. };
  25488. }
  25489. function makeDurationAsGetter(name, factor) {
  25490. moment.duration.fn['as' + name] = function () {
  25491. return +this / factor;
  25492. };
  25493. }
  25494. for (i in unitMillisecondFactors) {
  25495. if (unitMillisecondFactors.hasOwnProperty(i)) {
  25496. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  25497. makeDurationGetter(i.toLowerCase());
  25498. }
  25499. }
  25500. makeDurationAsGetter('Weeks', 6048e5);
  25501. moment.duration.fn.asMonths = function () {
  25502. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  25503. };
  25504. /************************************
  25505. Default Lang
  25506. ************************************/
  25507. // Set default language, other languages will inherit from English.
  25508. moment.lang('en', {
  25509. ordinal : function (number) {
  25510. var b = number % 10,
  25511. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  25512. (b === 1) ? 'st' :
  25513. (b === 2) ? 'nd' :
  25514. (b === 3) ? 'rd' : 'th';
  25515. return number + output;
  25516. }
  25517. });
  25518. /* EMBED_LANGUAGES */
  25519. /************************************
  25520. Exposing Moment
  25521. ************************************/
  25522. function makeGlobal(shouldDeprecate) {
  25523. /*global ender:false */
  25524. if (typeof ender !== 'undefined') {
  25525. return;
  25526. }
  25527. oldGlobalMoment = globalScope.moment;
  25528. if (shouldDeprecate) {
  25529. globalScope.moment = deprecate(
  25530. "Accessing Moment through the global scope is " +
  25531. "deprecated, and will be removed in an upcoming " +
  25532. "release.",
  25533. moment);
  25534. } else {
  25535. globalScope.moment = moment;
  25536. }
  25537. }
  25538. // CommonJS module is defined
  25539. if (hasModule) {
  25540. module.exports = moment;
  25541. } else if (typeof define === "function" && define.amd) {
  25542. define("moment", function (require, exports, module) {
  25543. if (module.config && module.config() && module.config().noGlobal === true) {
  25544. // release the global variable
  25545. globalScope.moment = oldGlobalMoment;
  25546. }
  25547. return moment;
  25548. });
  25549. makeGlobal(true);
  25550. } else {
  25551. makeGlobal();
  25552. }
  25553. }).call(this);
  25554. },{}],5:[function(require,module,exports){
  25555. /**
  25556. * Copyright 2012 Craig Campbell
  25557. *
  25558. * Licensed under the Apache License, Version 2.0 (the "License");
  25559. * you may not use this file except in compliance with the License.
  25560. * You may obtain a copy of the License at
  25561. *
  25562. * http://www.apache.org/licenses/LICENSE-2.0
  25563. *
  25564. * Unless required by applicable law or agreed to in writing, software
  25565. * distributed under the License is distributed on an "AS IS" BASIS,
  25566. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  25567. * See the License for the specific language governing permissions and
  25568. * limitations under the License.
  25569. *
  25570. * Mousetrap is a simple keyboard shortcut library for Javascript with
  25571. * no external dependencies
  25572. *
  25573. * @version 1.1.2
  25574. * @url craig.is/killing/mice
  25575. */
  25576. /**
  25577. * mapping of special keycodes to their corresponding keys
  25578. *
  25579. * everything in this dictionary cannot use keypress events
  25580. * so it has to be here to map to the correct keycodes for
  25581. * keyup/keydown events
  25582. *
  25583. * @type {Object}
  25584. */
  25585. var _MAP = {
  25586. 8: 'backspace',
  25587. 9: 'tab',
  25588. 13: 'enter',
  25589. 16: 'shift',
  25590. 17: 'ctrl',
  25591. 18: 'alt',
  25592. 20: 'capslock',
  25593. 27: 'esc',
  25594. 32: 'space',
  25595. 33: 'pageup',
  25596. 34: 'pagedown',
  25597. 35: 'end',
  25598. 36: 'home',
  25599. 37: 'left',
  25600. 38: 'up',
  25601. 39: 'right',
  25602. 40: 'down',
  25603. 45: 'ins',
  25604. 46: 'del',
  25605. 91: 'meta',
  25606. 93: 'meta',
  25607. 224: 'meta'
  25608. },
  25609. /**
  25610. * mapping for special characters so they can support
  25611. *
  25612. * this dictionary is only used incase you want to bind a
  25613. * keyup or keydown event to one of these keys
  25614. *
  25615. * @type {Object}
  25616. */
  25617. _KEYCODE_MAP = {
  25618. 106: '*',
  25619. 107: '+',
  25620. 109: '-',
  25621. 110: '.',
  25622. 111 : '/',
  25623. 186: ';',
  25624. 187: '=',
  25625. 188: ',',
  25626. 189: '-',
  25627. 190: '.',
  25628. 191: '/',
  25629. 192: '`',
  25630. 219: '[',
  25631. 220: '\\',
  25632. 221: ']',
  25633. 222: '\''
  25634. },
  25635. /**
  25636. * this is a mapping of keys that require shift on a US keypad
  25637. * back to the non shift equivelents
  25638. *
  25639. * this is so you can use keyup events with these keys
  25640. *
  25641. * note that this will only work reliably on US keyboards
  25642. *
  25643. * @type {Object}
  25644. */
  25645. _SHIFT_MAP = {
  25646. '~': '`',
  25647. '!': '1',
  25648. '@': '2',
  25649. '#': '3',
  25650. '$': '4',
  25651. '%': '5',
  25652. '^': '6',
  25653. '&': '7',
  25654. '*': '8',
  25655. '(': '9',
  25656. ')': '0',
  25657. '_': '-',
  25658. '+': '=',
  25659. ':': ';',
  25660. '\"': '\'',
  25661. '<': ',',
  25662. '>': '.',
  25663. '?': '/',
  25664. '|': '\\'
  25665. },
  25666. /**
  25667. * this is a list of special strings you can use to map
  25668. * to modifier keys when you specify your keyboard shortcuts
  25669. *
  25670. * @type {Object}
  25671. */
  25672. _SPECIAL_ALIASES = {
  25673. 'option': 'alt',
  25674. 'command': 'meta',
  25675. 'return': 'enter',
  25676. 'escape': 'esc'
  25677. },
  25678. /**
  25679. * variable to store the flipped version of _MAP from above
  25680. * needed to check if we should use keypress or not when no action
  25681. * is specified
  25682. *
  25683. * @type {Object|undefined}
  25684. */
  25685. _REVERSE_MAP,
  25686. /**
  25687. * a list of all the callbacks setup via Mousetrap.bind()
  25688. *
  25689. * @type {Object}
  25690. */
  25691. _callbacks = {},
  25692. /**
  25693. * direct map of string combinations to callbacks used for trigger()
  25694. *
  25695. * @type {Object}
  25696. */
  25697. _direct_map = {},
  25698. /**
  25699. * keeps track of what level each sequence is at since multiple
  25700. * sequences can start out with the same sequence
  25701. *
  25702. * @type {Object}
  25703. */
  25704. _sequence_levels = {},
  25705. /**
  25706. * variable to store the setTimeout call
  25707. *
  25708. * @type {null|number}
  25709. */
  25710. _reset_timer,
  25711. /**
  25712. * temporary state where we will ignore the next keyup
  25713. *
  25714. * @type {boolean|string}
  25715. */
  25716. _ignore_next_keyup = false,
  25717. /**
  25718. * are we currently inside of a sequence?
  25719. * type of action ("keyup" or "keydown" or "keypress") or false
  25720. *
  25721. * @type {boolean|string}
  25722. */
  25723. _inside_sequence = false;
  25724. /**
  25725. * loop through the f keys, f1 to f19 and add them to the map
  25726. * programatically
  25727. */
  25728. for (var i = 1; i < 20; ++i) {
  25729. _MAP[111 + i] = 'f' + i;
  25730. }
  25731. /**
  25732. * loop through to map numbers on the numeric keypad
  25733. */
  25734. for (i = 0; i <= 9; ++i) {
  25735. _MAP[i + 96] = i;
  25736. }
  25737. /**
  25738. * cross browser add event method
  25739. *
  25740. * @param {Element|HTMLDocument} object
  25741. * @param {string} type
  25742. * @param {Function} callback
  25743. * @returns void
  25744. */
  25745. function _addEvent(object, type, callback) {
  25746. if (object.addEventListener) {
  25747. return object.addEventListener(type, callback, false);
  25748. }
  25749. object.attachEvent('on' + type, callback);
  25750. }
  25751. /**
  25752. * takes the event and returns the key character
  25753. *
  25754. * @param {Event} e
  25755. * @return {string}
  25756. */
  25757. function _characterFromEvent(e) {
  25758. // for keypress events we should return the character as is
  25759. if (e.type == 'keypress') {
  25760. return String.fromCharCode(e.which);
  25761. }
  25762. // for non keypress events the special maps are needed
  25763. if (_MAP[e.which]) {
  25764. return _MAP[e.which];
  25765. }
  25766. if (_KEYCODE_MAP[e.which]) {
  25767. return _KEYCODE_MAP[e.which];
  25768. }
  25769. // if it is not in the special map
  25770. return String.fromCharCode(e.which).toLowerCase();
  25771. }
  25772. /**
  25773. * should we stop this event before firing off callbacks
  25774. *
  25775. * @param {Event} e
  25776. * @return {boolean}
  25777. */
  25778. function _stop(e) {
  25779. var element = e.target || e.srcElement,
  25780. tag_name = element.tagName;
  25781. // if the element has the class "mousetrap" then no need to stop
  25782. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  25783. return false;
  25784. }
  25785. // stop for input, select, and textarea
  25786. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  25787. }
  25788. /**
  25789. * checks if two arrays are equal
  25790. *
  25791. * @param {Array} modifiers1
  25792. * @param {Array} modifiers2
  25793. * @returns {boolean}
  25794. */
  25795. function _modifiersMatch(modifiers1, modifiers2) {
  25796. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  25797. }
  25798. /**
  25799. * resets all sequence counters except for the ones passed in
  25800. *
  25801. * @param {Object} do_not_reset
  25802. * @returns void
  25803. */
  25804. function _resetSequences(do_not_reset) {
  25805. do_not_reset = do_not_reset || {};
  25806. var active_sequences = false,
  25807. key;
  25808. for (key in _sequence_levels) {
  25809. if (do_not_reset[key]) {
  25810. active_sequences = true;
  25811. continue;
  25812. }
  25813. _sequence_levels[key] = 0;
  25814. }
  25815. if (!active_sequences) {
  25816. _inside_sequence = false;
  25817. }
  25818. }
  25819. /**
  25820. * finds all callbacks that match based on the keycode, modifiers,
  25821. * and action
  25822. *
  25823. * @param {string} character
  25824. * @param {Array} modifiers
  25825. * @param {string} action
  25826. * @param {boolean=} remove - should we remove any matches
  25827. * @param {string=} combination
  25828. * @returns {Array}
  25829. */
  25830. function _getMatches(character, modifiers, action, remove, combination) {
  25831. var i,
  25832. callback,
  25833. matches = [];
  25834. // if there are no events related to this keycode
  25835. if (!_callbacks[character]) {
  25836. return [];
  25837. }
  25838. // if a modifier key is coming up on its own we should allow it
  25839. if (action == 'keyup' && _isModifier(character)) {
  25840. modifiers = [character];
  25841. }
  25842. // loop through all callbacks for the key that was pressed
  25843. // and see if any of them match
  25844. for (i = 0; i < _callbacks[character].length; ++i) {
  25845. callback = _callbacks[character][i];
  25846. // if this is a sequence but it is not at the right level
  25847. // then move onto the next match
  25848. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  25849. continue;
  25850. }
  25851. // if the action we are looking for doesn't match the action we got
  25852. // then we should keep going
  25853. if (action != callback.action) {
  25854. continue;
  25855. }
  25856. // if this is a keypress event that means that we need to only
  25857. // look at the character, otherwise check the modifiers as
  25858. // well
  25859. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  25860. // remove is used so if you change your mind and call bind a
  25861. // second time with a new function the first one is overwritten
  25862. if (remove && callback.combo == combination) {
  25863. _callbacks[character].splice(i, 1);
  25864. }
  25865. matches.push(callback);
  25866. }
  25867. }
  25868. return matches;
  25869. }
  25870. /**
  25871. * takes a key event and figures out what the modifiers are
  25872. *
  25873. * @param {Event} e
  25874. * @returns {Array}
  25875. */
  25876. function _eventModifiers(e) {
  25877. var modifiers = [];
  25878. if (e.shiftKey) {
  25879. modifiers.push('shift');
  25880. }
  25881. if (e.altKey) {
  25882. modifiers.push('alt');
  25883. }
  25884. if (e.ctrlKey) {
  25885. modifiers.push('ctrl');
  25886. }
  25887. if (e.metaKey) {
  25888. modifiers.push('meta');
  25889. }
  25890. return modifiers;
  25891. }
  25892. /**
  25893. * actually calls the callback function
  25894. *
  25895. * if your callback function returns false this will use the jquery
  25896. * convention - prevent default and stop propogation on the event
  25897. *
  25898. * @param {Function} callback
  25899. * @param {Event} e
  25900. * @returns void
  25901. */
  25902. function _fireCallback(callback, e) {
  25903. if (callback(e) === false) {
  25904. if (e.preventDefault) {
  25905. e.preventDefault();
  25906. }
  25907. if (e.stopPropagation) {
  25908. e.stopPropagation();
  25909. }
  25910. e.returnValue = false;
  25911. e.cancelBubble = true;
  25912. }
  25913. }
  25914. /**
  25915. * handles a character key event
  25916. *
  25917. * @param {string} character
  25918. * @param {Event} e
  25919. * @returns void
  25920. */
  25921. function _handleCharacter(character, e) {
  25922. // if this event should not happen stop here
  25923. if (_stop(e)) {
  25924. return;
  25925. }
  25926. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  25927. i,
  25928. do_not_reset = {},
  25929. processed_sequence_callback = false;
  25930. // loop through matching callbacks for this key event
  25931. for (i = 0; i < callbacks.length; ++i) {
  25932. // fire for all sequence callbacks
  25933. // this is because if for example you have multiple sequences
  25934. // bound such as "g i" and "g t" they both need to fire the
  25935. // callback for matching g cause otherwise you can only ever
  25936. // match the first one
  25937. if (callbacks[i].seq) {
  25938. processed_sequence_callback = true;
  25939. // keep a list of which sequences were matches for later
  25940. do_not_reset[callbacks[i].seq] = 1;
  25941. _fireCallback(callbacks[i].callback, e);
  25942. continue;
  25943. }
  25944. // if there were no sequence matches but we are still here
  25945. // that means this is a regular match so we should fire that
  25946. if (!processed_sequence_callback && !_inside_sequence) {
  25947. _fireCallback(callbacks[i].callback, e);
  25948. }
  25949. }
  25950. // if you are inside of a sequence and the key you are pressing
  25951. // is not a modifier key then we should reset all sequences
  25952. // that were not matched by this key event
  25953. if (e.type == _inside_sequence && !_isModifier(character)) {
  25954. _resetSequences(do_not_reset);
  25955. }
  25956. }
  25957. /**
  25958. * handles a keydown event
  25959. *
  25960. * @param {Event} e
  25961. * @returns void
  25962. */
  25963. function _handleKey(e) {
  25964. // normalize e.which for key events
  25965. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  25966. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  25967. var character = _characterFromEvent(e);
  25968. // no character found then stop
  25969. if (!character) {
  25970. return;
  25971. }
  25972. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  25973. _ignore_next_keyup = false;
  25974. return;
  25975. }
  25976. _handleCharacter(character, e);
  25977. }
  25978. /**
  25979. * determines if the keycode specified is a modifier key or not
  25980. *
  25981. * @param {string} key
  25982. * @returns {boolean}
  25983. */
  25984. function _isModifier(key) {
  25985. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  25986. }
  25987. /**
  25988. * called to set a 1 second timeout on the specified sequence
  25989. *
  25990. * this is so after each key press in the sequence you have 1 second
  25991. * to press the next key before you have to start over
  25992. *
  25993. * @returns void
  25994. */
  25995. function _resetSequenceTimer() {
  25996. clearTimeout(_reset_timer);
  25997. _reset_timer = setTimeout(_resetSequences, 1000);
  25998. }
  25999. /**
  26000. * reverses the map lookup so that we can look for specific keys
  26001. * to see what can and can't use keypress
  26002. *
  26003. * @return {Object}
  26004. */
  26005. function _getReverseMap() {
  26006. if (!_REVERSE_MAP) {
  26007. _REVERSE_MAP = {};
  26008. for (var key in _MAP) {
  26009. // pull out the numeric keypad from here cause keypress should
  26010. // be able to detect the keys from the character
  26011. if (key > 95 && key < 112) {
  26012. continue;
  26013. }
  26014. if (_MAP.hasOwnProperty(key)) {
  26015. _REVERSE_MAP[_MAP[key]] = key;
  26016. }
  26017. }
  26018. }
  26019. return _REVERSE_MAP;
  26020. }
  26021. /**
  26022. * picks the best action based on the key combination
  26023. *
  26024. * @param {string} key - character for key
  26025. * @param {Array} modifiers
  26026. * @param {string=} action passed in
  26027. */
  26028. function _pickBestAction(key, modifiers, action) {
  26029. // if no action was picked in we should try to pick the one
  26030. // that we think would work best for this key
  26031. if (!action) {
  26032. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  26033. }
  26034. // modifier keys don't work as expected with keypress,
  26035. // switch to keydown
  26036. if (action == 'keypress' && modifiers.length) {
  26037. action = 'keydown';
  26038. }
  26039. return action;
  26040. }
  26041. /**
  26042. * binds a key sequence to an event
  26043. *
  26044. * @param {string} combo - combo specified in bind call
  26045. * @param {Array} keys
  26046. * @param {Function} callback
  26047. * @param {string=} action
  26048. * @returns void
  26049. */
  26050. function _bindSequence(combo, keys, callback, action) {
  26051. // start off by adding a sequence level record for this combination
  26052. // and setting the level to 0
  26053. _sequence_levels[combo] = 0;
  26054. // if there is no action pick the best one for the first key
  26055. // in the sequence
  26056. if (!action) {
  26057. action = _pickBestAction(keys[0], []);
  26058. }
  26059. /**
  26060. * callback to increase the sequence level for this sequence and reset
  26061. * all other sequences that were active
  26062. *
  26063. * @param {Event} e
  26064. * @returns void
  26065. */
  26066. var _increaseSequence = function(e) {
  26067. _inside_sequence = action;
  26068. ++_sequence_levels[combo];
  26069. _resetSequenceTimer();
  26070. },
  26071. /**
  26072. * wraps the specified callback inside of another function in order
  26073. * to reset all sequence counters as soon as this sequence is done
  26074. *
  26075. * @param {Event} e
  26076. * @returns void
  26077. */
  26078. _callbackAndReset = function(e) {
  26079. _fireCallback(callback, e);
  26080. // we should ignore the next key up if the action is key down
  26081. // or keypress. this is so if you finish a sequence and
  26082. // release the key the final key will not trigger a keyup
  26083. if (action !== 'keyup') {
  26084. _ignore_next_keyup = _characterFromEvent(e);
  26085. }
  26086. // weird race condition if a sequence ends with the key
  26087. // another sequence begins with
  26088. setTimeout(_resetSequences, 10);
  26089. },
  26090. i;
  26091. // loop through keys one at a time and bind the appropriate callback
  26092. // function. for any key leading up to the final one it should
  26093. // increase the sequence. after the final, it should reset all sequences
  26094. for (i = 0; i < keys.length; ++i) {
  26095. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  26096. }
  26097. }
  26098. /**
  26099. * binds a single keyboard combination
  26100. *
  26101. * @param {string} combination
  26102. * @param {Function} callback
  26103. * @param {string=} action
  26104. * @param {string=} sequence_name - name of sequence if part of sequence
  26105. * @param {number=} level - what part of the sequence the command is
  26106. * @returns void
  26107. */
  26108. function _bindSingle(combination, callback, action, sequence_name, level) {
  26109. // make sure multiple spaces in a row become a single space
  26110. combination = combination.replace(/\s+/g, ' ');
  26111. var sequence = combination.split(' '),
  26112. i,
  26113. key,
  26114. keys,
  26115. modifiers = [];
  26116. // if this pattern is a sequence of keys then run through this method
  26117. // to reprocess each pattern one key at a time
  26118. if (sequence.length > 1) {
  26119. return _bindSequence(combination, sequence, callback, action);
  26120. }
  26121. // take the keys from this pattern and figure out what the actual
  26122. // pattern is all about
  26123. keys = combination === '+' ? ['+'] : combination.split('+');
  26124. for (i = 0; i < keys.length; ++i) {
  26125. key = keys[i];
  26126. // normalize key names
  26127. if (_SPECIAL_ALIASES[key]) {
  26128. key = _SPECIAL_ALIASES[key];
  26129. }
  26130. // if this is not a keypress event then we should
  26131. // be smart about using shift keys
  26132. // this will only work for US keyboards however
  26133. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  26134. key = _SHIFT_MAP[key];
  26135. modifiers.push('shift');
  26136. }
  26137. // if this key is a modifier then add it to the list of modifiers
  26138. if (_isModifier(key)) {
  26139. modifiers.push(key);
  26140. }
  26141. }
  26142. // depending on what the key combination is
  26143. // we will try to pick the best event for it
  26144. action = _pickBestAction(key, modifiers, action);
  26145. // make sure to initialize array if this is the first time
  26146. // a callback is added for this key
  26147. if (!_callbacks[key]) {
  26148. _callbacks[key] = [];
  26149. }
  26150. // remove an existing match if there is one
  26151. _getMatches(key, modifiers, action, !sequence_name, combination);
  26152. // add this call back to the array
  26153. // if it is a sequence put it at the beginning
  26154. // if not put it at the end
  26155. //
  26156. // this is important because the way these are processed expects
  26157. // the sequence ones to come first
  26158. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  26159. callback: callback,
  26160. modifiers: modifiers,
  26161. action: action,
  26162. seq: sequence_name,
  26163. level: level,
  26164. combo: combination
  26165. });
  26166. }
  26167. /**
  26168. * binds multiple combinations to the same callback
  26169. *
  26170. * @param {Array} combinations
  26171. * @param {Function} callback
  26172. * @param {string|undefined} action
  26173. * @returns void
  26174. */
  26175. function _bindMultiple(combinations, callback, action) {
  26176. for (var i = 0; i < combinations.length; ++i) {
  26177. _bindSingle(combinations[i], callback, action);
  26178. }
  26179. }
  26180. // start!
  26181. _addEvent(document, 'keypress', _handleKey);
  26182. _addEvent(document, 'keydown', _handleKey);
  26183. _addEvent(document, 'keyup', _handleKey);
  26184. var mousetrap = {
  26185. /**
  26186. * binds an event to mousetrap
  26187. *
  26188. * can be a single key, a combination of keys separated with +,
  26189. * a comma separated list of keys, an array of keys, or
  26190. * a sequence of keys separated by spaces
  26191. *
  26192. * be sure to list the modifier keys first to make sure that the
  26193. * correct key ends up getting bound (the last key in the pattern)
  26194. *
  26195. * @param {string|Array} keys
  26196. * @param {Function} callback
  26197. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  26198. * @returns void
  26199. */
  26200. bind: function(keys, callback, action) {
  26201. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  26202. _direct_map[keys + ':' + action] = callback;
  26203. return this;
  26204. },
  26205. /**
  26206. * unbinds an event to mousetrap
  26207. *
  26208. * the unbinding sets the callback function of the specified key combo
  26209. * to an empty function and deletes the corresponding key in the
  26210. * _direct_map dict.
  26211. *
  26212. * the keycombo+action has to be exactly the same as
  26213. * it was defined in the bind method
  26214. *
  26215. * TODO: actually remove this from the _callbacks dictionary instead
  26216. * of binding an empty function
  26217. *
  26218. * @param {string|Array} keys
  26219. * @param {string} action
  26220. * @returns void
  26221. */
  26222. unbind: function(keys, action) {
  26223. if (_direct_map[keys + ':' + action]) {
  26224. delete _direct_map[keys + ':' + action];
  26225. this.bind(keys, function() {}, action);
  26226. }
  26227. return this;
  26228. },
  26229. /**
  26230. * triggers an event that has already been bound
  26231. *
  26232. * @param {string} keys
  26233. * @param {string=} action
  26234. * @returns void
  26235. */
  26236. trigger: function(keys, action) {
  26237. _direct_map[keys + ':' + action]();
  26238. return this;
  26239. },
  26240. /**
  26241. * resets the library back to its initial state. this is useful
  26242. * if you want to clear out the current keyboard shortcuts and bind
  26243. * new ones - for example if you switch to another page
  26244. *
  26245. * @returns void
  26246. */
  26247. reset: function() {
  26248. _callbacks = {};
  26249. _direct_map = {};
  26250. return this;
  26251. }
  26252. };
  26253. module.exports = mousetrap;
  26254. },{}]},{},[1])
  26255. (1)
  26256. });