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.

17549 lines
514 KiB

  1. /**
  2. * vis.js module imports
  3. */
  4. // Try to load dependencies from the global window object.
  5. // If not available there, load via require.
  6. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  7. var Emitter = require('emitter-component');
  8. var Hammer;
  9. if (typeof window !== 'undefined') {
  10. // load hammer.js only when running in a browser (where window is available)
  11. Hammer = window['Hammer'] || require('hammerjs');
  12. }
  13. else {
  14. Hammer = function () {
  15. throw Error('hammer.js is only available in a browser, not in node.js.');
  16. }
  17. }
  18. var mousetrap;
  19. if (typeof window !== 'undefined') {
  20. // load mousetrap.js only when running in a browser (where window is available)
  21. mousetrap = window['mousetrap'] || require('mousetrap');
  22. }
  23. else {
  24. mousetrap = function () {
  25. throw Error('mouseTrap is only available in a browser, not in node.js.');
  26. }
  27. }
  28. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  29. // it here in that case.
  30. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  31. if(!Array.prototype.indexOf) {
  32. Array.prototype.indexOf = function(obj){
  33. for(var i = 0; i < this.length; i++){
  34. if(this[i] == obj){
  35. return i;
  36. }
  37. }
  38. return -1;
  39. };
  40. try {
  41. console.log("Warning: Ancient browser detected. Please update your browser");
  42. }
  43. catch (err) {
  44. }
  45. }
  46. // Internet Explorer 8 and older does not support Array.forEach, so we define
  47. // it here in that case.
  48. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  49. if (!Array.prototype.forEach) {
  50. Array.prototype.forEach = function(fn, scope) {
  51. for(var i = 0, len = this.length; i < len; ++i) {
  52. fn.call(scope || this, this[i], i, this);
  53. }
  54. }
  55. }
  56. // Internet Explorer 8 and older does not support Array.map, so we define it
  57. // here in that case.
  58. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  59. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  60. // Reference: http://es5.github.com/#x15.4.4.19
  61. if (!Array.prototype.map) {
  62. Array.prototype.map = function(callback, thisArg) {
  63. var T, A, k;
  64. if (this == null) {
  65. throw new TypeError(" this is null or not defined");
  66. }
  67. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  68. var O = Object(this);
  69. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  70. // 3. Let len be ToUint32(lenValue).
  71. var len = O.length >>> 0;
  72. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  73. // See: http://es5.github.com/#x9.11
  74. if (typeof callback !== "function") {
  75. throw new TypeError(callback + " is not a function");
  76. }
  77. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  78. if (thisArg) {
  79. T = thisArg;
  80. }
  81. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  82. // the standard built-in constructor with that name and len is the value of len.
  83. A = new Array(len);
  84. // 7. Let k be 0
  85. k = 0;
  86. // 8. Repeat, while k < len
  87. while(k < len) {
  88. var kValue, mappedValue;
  89. // a. Let Pk be ToString(k).
  90. // This is implicit for LHS operands of the in operator
  91. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  92. // This step can be combined with c
  93. // c. If kPresent is true, then
  94. if (k in O) {
  95. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  96. kValue = O[ k ];
  97. // ii. Let mappedValue be the result of calling the Call internal method of callback
  98. // with T as the this value and argument list containing kValue, k, and O.
  99. mappedValue = callback.call(T, kValue, k, O);
  100. // iii. Call the DefineOwnProperty internal method of A with arguments
  101. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  102. // and false.
  103. // In browsers that support Object.defineProperty, use the following:
  104. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  105. // For best browser support, use the following:
  106. A[ k ] = mappedValue;
  107. }
  108. // d. Increase k by 1.
  109. k++;
  110. }
  111. // 9. return A
  112. return A;
  113. };
  114. }
  115. // Internet Explorer 8 and older does not support Array.filter, so we define it
  116. // here in that case.
  117. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  118. if (!Array.prototype.filter) {
  119. Array.prototype.filter = function(fun /*, thisp */) {
  120. "use strict";
  121. if (this == null) {
  122. throw new TypeError();
  123. }
  124. var t = Object(this);
  125. var len = t.length >>> 0;
  126. if (typeof fun != "function") {
  127. throw new TypeError();
  128. }
  129. var res = [];
  130. var thisp = arguments[1];
  131. for (var i = 0; i < len; i++) {
  132. if (i in t) {
  133. var val = t[i]; // in case fun mutates this
  134. if (fun.call(thisp, val, i, t))
  135. res.push(val);
  136. }
  137. }
  138. return res;
  139. };
  140. }
  141. // Internet Explorer 8 and older does not support Object.keys, so we define it
  142. // here in that case.
  143. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  144. if (!Object.keys) {
  145. Object.keys = (function () {
  146. var hasOwnProperty = Object.prototype.hasOwnProperty,
  147. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  148. dontEnums = [
  149. 'toString',
  150. 'toLocaleString',
  151. 'valueOf',
  152. 'hasOwnProperty',
  153. 'isPrototypeOf',
  154. 'propertyIsEnumerable',
  155. 'constructor'
  156. ],
  157. dontEnumsLength = dontEnums.length;
  158. return function (obj) {
  159. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  160. throw new TypeError('Object.keys called on non-object');
  161. }
  162. var result = [];
  163. for (var prop in obj) {
  164. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  165. }
  166. if (hasDontEnumBug) {
  167. for (var i=0; i < dontEnumsLength; i++) {
  168. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  169. }
  170. }
  171. return result;
  172. }
  173. })()
  174. }
  175. // Internet Explorer 8 and older does not support Array.isArray,
  176. // so we define it here in that case.
  177. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  178. if(!Array.isArray) {
  179. Array.isArray = function (vArg) {
  180. return Object.prototype.toString.call(vArg) === "[object Array]";
  181. };
  182. }
  183. // Internet Explorer 8 and older does not support Function.bind,
  184. // so we define it here in that case.
  185. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  186. if (!Function.prototype.bind) {
  187. Function.prototype.bind = function (oThis) {
  188. if (typeof this !== "function") {
  189. // closest thing possible to the ECMAScript 5 internal IsCallable function
  190. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  191. }
  192. var aArgs = Array.prototype.slice.call(arguments, 1),
  193. fToBind = this,
  194. fNOP = function () {},
  195. fBound = function () {
  196. return fToBind.apply(this instanceof fNOP && oThis
  197. ? this
  198. : oThis,
  199. aArgs.concat(Array.prototype.slice.call(arguments)));
  200. };
  201. fNOP.prototype = this.prototype;
  202. fBound.prototype = new fNOP();
  203. return fBound;
  204. };
  205. }
  206. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  207. if (!Object.create) {
  208. Object.create = function (o) {
  209. if (arguments.length > 1) {
  210. throw new Error('Object.create implementation only accepts the first parameter.');
  211. }
  212. function F() {}
  213. F.prototype = o;
  214. return new F();
  215. };
  216. }
  217. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  218. if (!Function.prototype.bind) {
  219. Function.prototype.bind = function (oThis) {
  220. if (typeof this !== "function") {
  221. // closest thing possible to the ECMAScript 5 internal IsCallable function
  222. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  223. }
  224. var aArgs = Array.prototype.slice.call(arguments, 1),
  225. fToBind = this,
  226. fNOP = function () {},
  227. fBound = function () {
  228. return fToBind.apply(this instanceof fNOP && oThis
  229. ? this
  230. : oThis,
  231. aArgs.concat(Array.prototype.slice.call(arguments)));
  232. };
  233. fNOP.prototype = this.prototype;
  234. fBound.prototype = new fNOP();
  235. return fBound;
  236. };
  237. }
  238. /**
  239. * utility functions
  240. */
  241. var util = {};
  242. /**
  243. * Test whether given object is a number
  244. * @param {*} object
  245. * @return {Boolean} isNumber
  246. */
  247. util.isNumber = function isNumber(object) {
  248. return (object instanceof Number || typeof object == 'number');
  249. };
  250. /**
  251. * Test whether given object is a string
  252. * @param {*} object
  253. * @return {Boolean} isString
  254. */
  255. util.isString = function isString(object) {
  256. return (object instanceof String || typeof object == 'string');
  257. };
  258. /**
  259. * Test whether given object is a Date, or a String containing a Date
  260. * @param {Date | String} object
  261. * @return {Boolean} isDate
  262. */
  263. util.isDate = function isDate(object) {
  264. if (object instanceof Date) {
  265. return true;
  266. }
  267. else if (util.isString(object)) {
  268. // test whether this string contains a date
  269. var match = ASPDateRegex.exec(object);
  270. if (match) {
  271. return true;
  272. }
  273. else if (!isNaN(Date.parse(object))) {
  274. return true;
  275. }
  276. }
  277. return false;
  278. };
  279. /**
  280. * Test whether given object is an instance of google.visualization.DataTable
  281. * @param {*} object
  282. * @return {Boolean} isDataTable
  283. */
  284. util.isDataTable = function isDataTable(object) {
  285. return (typeof (google) !== 'undefined') &&
  286. (google.visualization) &&
  287. (google.visualization.DataTable) &&
  288. (object instanceof google.visualization.DataTable);
  289. };
  290. /**
  291. * Create a semi UUID
  292. * source: http://stackoverflow.com/a/105074/1262753
  293. * @return {String} uuid
  294. */
  295. util.randomUUID = function randomUUID () {
  296. var S4 = function () {
  297. return Math.floor(
  298. Math.random() * 0x10000 /* 65536 */
  299. ).toString(16);
  300. };
  301. return (
  302. S4() + S4() + '-' +
  303. S4() + '-' +
  304. S4() + '-' +
  305. S4() + '-' +
  306. S4() + S4() + S4()
  307. );
  308. };
  309. /**
  310. * Extend object a with the properties of object b or a series of objects
  311. * Only properties with defined values are copied
  312. * @param {Object} a
  313. * @param {... Object} b
  314. * @return {Object} a
  315. */
  316. util.extend = function (a, b) {
  317. for (var i = 1, len = arguments.length; i < len; i++) {
  318. var other = arguments[i];
  319. for (var prop in other) {
  320. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  321. a[prop] = other[prop];
  322. }
  323. }
  324. }
  325. return a;
  326. };
  327. /**
  328. * Test whether all elements in two arrays are equal.
  329. * @param {Array} a
  330. * @param {Array} b
  331. * @return {boolean} Returns true if both arrays have the same length and same
  332. * elements.
  333. */
  334. util.equalArray = function (a, b) {
  335. if (a.length != b.length) return false;
  336. for (var i = 1, len = a.length; i < len; i++) {
  337. if (a[i] != b[i]) return false;
  338. }
  339. return true;
  340. };
  341. /**
  342. * Convert an object to another type
  343. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  344. * @param {String | undefined} type Name of the type. Available types:
  345. * 'Boolean', 'Number', 'String',
  346. * 'Date', 'Moment', ISODate', 'ASPDate'.
  347. * @return {*} object
  348. * @throws Error
  349. */
  350. util.convert = function convert(object, type) {
  351. var match;
  352. if (object === undefined) {
  353. return undefined;
  354. }
  355. if (object === null) {
  356. return null;
  357. }
  358. if (!type) {
  359. return object;
  360. }
  361. if (!(typeof type === 'string') && !(type instanceof String)) {
  362. throw new Error('Type must be a string');
  363. }
  364. //noinspection FallthroughInSwitchStatementJS
  365. switch (type) {
  366. case 'boolean':
  367. case 'Boolean':
  368. return Boolean(object);
  369. case 'number':
  370. case 'Number':
  371. return Number(object.valueOf());
  372. case 'string':
  373. case 'String':
  374. return String(object);
  375. case 'Date':
  376. if (util.isNumber(object)) {
  377. return new Date(object);
  378. }
  379. if (object instanceof Date) {
  380. return new Date(object.valueOf());
  381. }
  382. else if (moment.isMoment(object)) {
  383. return new Date(object.valueOf());
  384. }
  385. if (util.isString(object)) {
  386. match = ASPDateRegex.exec(object);
  387. if (match) {
  388. // object is an ASP date
  389. return new Date(Number(match[1])); // parse number
  390. }
  391. else {
  392. return moment(object).toDate(); // parse string
  393. }
  394. }
  395. else {
  396. throw new Error(
  397. 'Cannot convert object of type ' + util.getType(object) +
  398. ' to type Date');
  399. }
  400. case 'Moment':
  401. if (util.isNumber(object)) {
  402. return moment(object);
  403. }
  404. if (object instanceof Date) {
  405. return moment(object.valueOf());
  406. }
  407. else if (moment.isMoment(object)) {
  408. return moment(object);
  409. }
  410. if (util.isString(object)) {
  411. match = ASPDateRegex.exec(object);
  412. if (match) {
  413. // object is an ASP date
  414. return moment(Number(match[1])); // parse number
  415. }
  416. else {
  417. return moment(object); // parse string
  418. }
  419. }
  420. else {
  421. throw new Error(
  422. 'Cannot convert object of type ' + util.getType(object) +
  423. ' to type Date');
  424. }
  425. case 'ISODate':
  426. if (util.isNumber(object)) {
  427. return new Date(object);
  428. }
  429. else if (object instanceof Date) {
  430. return object.toISOString();
  431. }
  432. else if (moment.isMoment(object)) {
  433. return object.toDate().toISOString();
  434. }
  435. else if (util.isString(object)) {
  436. match = ASPDateRegex.exec(object);
  437. if (match) {
  438. // object is an ASP date
  439. return new Date(Number(match[1])).toISOString(); // parse number
  440. }
  441. else {
  442. return new Date(object).toISOString(); // parse string
  443. }
  444. }
  445. else {
  446. throw new Error(
  447. 'Cannot convert object of type ' + util.getType(object) +
  448. ' to type ISODate');
  449. }
  450. case 'ASPDate':
  451. if (util.isNumber(object)) {
  452. return '/Date(' + object + ')/';
  453. }
  454. else if (object instanceof Date) {
  455. return '/Date(' + object.valueOf() + ')/';
  456. }
  457. else if (util.isString(object)) {
  458. match = ASPDateRegex.exec(object);
  459. var value;
  460. if (match) {
  461. // object is an ASP date
  462. value = new Date(Number(match[1])).valueOf(); // parse number
  463. }
  464. else {
  465. value = new Date(object).valueOf(); // parse string
  466. }
  467. return '/Date(' + value + ')/';
  468. }
  469. else {
  470. throw new Error(
  471. 'Cannot convert object of type ' + util.getType(object) +
  472. ' to type ASPDate');
  473. }
  474. default:
  475. throw new Error('Cannot convert object of type ' + util.getType(object) +
  476. ' to type "' + type + '"');
  477. }
  478. };
  479. // parse ASP.Net Date pattern,
  480. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  481. // code from http://momentjs.com/
  482. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  483. /**
  484. * Get the type of an object, for example util.getType([]) returns 'Array'
  485. * @param {*} object
  486. * @return {String} type
  487. */
  488. util.getType = function getType(object) {
  489. var type = typeof object;
  490. if (type == 'object') {
  491. if (object == null) {
  492. return 'null';
  493. }
  494. if (object instanceof Boolean) {
  495. return 'Boolean';
  496. }
  497. if (object instanceof Number) {
  498. return 'Number';
  499. }
  500. if (object instanceof String) {
  501. return 'String';
  502. }
  503. if (object instanceof Array) {
  504. return 'Array';
  505. }
  506. if (object instanceof Date) {
  507. return 'Date';
  508. }
  509. return 'Object';
  510. }
  511. else if (type == 'number') {
  512. return 'Number';
  513. }
  514. else if (type == 'boolean') {
  515. return 'Boolean';
  516. }
  517. else if (type == 'string') {
  518. return 'String';
  519. }
  520. return type;
  521. };
  522. /**
  523. * Retrieve the absolute left value of a DOM element
  524. * @param {Element} elem A dom element, for example a div
  525. * @return {number} left The absolute left position of this element
  526. * in the browser page.
  527. */
  528. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  529. var doc = document.documentElement;
  530. var body = document.body;
  531. var left = elem.offsetLeft;
  532. var e = elem.offsetParent;
  533. while (e != null && e != body && e != doc) {
  534. left += e.offsetLeft;
  535. left -= e.scrollLeft;
  536. e = e.offsetParent;
  537. }
  538. return left;
  539. };
  540. /**
  541. * Retrieve the absolute top value of a DOM element
  542. * @param {Element} elem A dom element, for example a div
  543. * @return {number} top The absolute top position of this element
  544. * in the browser page.
  545. */
  546. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  547. var doc = document.documentElement;
  548. var body = document.body;
  549. var top = elem.offsetTop;
  550. var e = elem.offsetParent;
  551. while (e != null && e != body && e != doc) {
  552. top += e.offsetTop;
  553. top -= e.scrollTop;
  554. e = e.offsetParent;
  555. }
  556. return top;
  557. };
  558. /**
  559. * Get the absolute, vertical mouse position from an event.
  560. * @param {Event} event
  561. * @return {Number} pageY
  562. */
  563. util.getPageY = function getPageY (event) {
  564. if ('pageY' in event) {
  565. return event.pageY;
  566. }
  567. else {
  568. var clientY;
  569. if (('targetTouches' in event) && event.targetTouches.length) {
  570. clientY = event.targetTouches[0].clientY;
  571. }
  572. else {
  573. clientY = event.clientY;
  574. }
  575. var doc = document.documentElement;
  576. var body = document.body;
  577. return clientY +
  578. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  579. ( doc && doc.clientTop || body && body.clientTop || 0 );
  580. }
  581. };
  582. /**
  583. * Get the absolute, horizontal mouse position from an event.
  584. * @param {Event} event
  585. * @return {Number} pageX
  586. */
  587. util.getPageX = function getPageX (event) {
  588. if ('pageY' in event) {
  589. return event.pageX;
  590. }
  591. else {
  592. var clientX;
  593. if (('targetTouches' in event) && event.targetTouches.length) {
  594. clientX = event.targetTouches[0].clientX;
  595. }
  596. else {
  597. clientX = event.clientX;
  598. }
  599. var doc = document.documentElement;
  600. var body = document.body;
  601. return clientX +
  602. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  603. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  604. }
  605. };
  606. /**
  607. * add a className to the given elements style
  608. * @param {Element} elem
  609. * @param {String} className
  610. */
  611. util.addClassName = function addClassName(elem, className) {
  612. var classes = elem.className.split(' ');
  613. if (classes.indexOf(className) == -1) {
  614. classes.push(className); // add the class to the array
  615. elem.className = classes.join(' ');
  616. }
  617. };
  618. /**
  619. * add a className to the given elements style
  620. * @param {Element} elem
  621. * @param {String} className
  622. */
  623. util.removeClassName = function removeClassname(elem, className) {
  624. var classes = elem.className.split(' ');
  625. var index = classes.indexOf(className);
  626. if (index != -1) {
  627. classes.splice(index, 1); // remove the class from the array
  628. elem.className = classes.join(' ');
  629. }
  630. };
  631. /**
  632. * For each method for both arrays and objects.
  633. * In case of an array, the built-in Array.forEach() is applied.
  634. * In case of an Object, the method loops over all properties of the object.
  635. * @param {Object | Array} object An Object or Array
  636. * @param {function} callback Callback method, called for each item in
  637. * the object or array with three parameters:
  638. * callback(value, index, object)
  639. */
  640. util.forEach = function forEach (object, callback) {
  641. var i,
  642. len;
  643. if (object instanceof Array) {
  644. // array
  645. for (i = 0, len = object.length; i < len; i++) {
  646. callback(object[i], i, object);
  647. }
  648. }
  649. else {
  650. // object
  651. for (i in object) {
  652. if (object.hasOwnProperty(i)) {
  653. callback(object[i], i, object);
  654. }
  655. }
  656. }
  657. };
  658. /**
  659. * Convert an object into an array: all objects properties are put into the
  660. * array. The resulting array is unordered.
  661. * @param {Object} object
  662. * @param {Array} array
  663. */
  664. util.toArray = function toArray(object) {
  665. var array = [];
  666. for (var prop in object) {
  667. if (object.hasOwnProperty(prop)) array.push(object[prop]);
  668. }
  669. return array;
  670. }
  671. /**
  672. * Update a property in an object
  673. * @param {Object} object
  674. * @param {String} key
  675. * @param {*} value
  676. * @return {Boolean} changed
  677. */
  678. util.updateProperty = function updateProperty (object, key, value) {
  679. if (object[key] !== value) {
  680. object[key] = value;
  681. return true;
  682. }
  683. else {
  684. return false;
  685. }
  686. };
  687. /**
  688. * Add and event listener. Works for all browsers
  689. * @param {Element} element An html element
  690. * @param {string} action The action, for example "click",
  691. * without the prefix "on"
  692. * @param {function} listener The callback function to be executed
  693. * @param {boolean} [useCapture]
  694. */
  695. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  696. if (element.addEventListener) {
  697. if (useCapture === undefined)
  698. useCapture = false;
  699. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  700. action = "DOMMouseScroll"; // For Firefox
  701. }
  702. element.addEventListener(action, listener, useCapture);
  703. } else {
  704. element.attachEvent("on" + action, listener); // IE browsers
  705. }
  706. };
  707. /**
  708. * Remove an event listener from an element
  709. * @param {Element} element An html dom element
  710. * @param {string} action The name of the event, for example "mousedown"
  711. * @param {function} listener The listener function
  712. * @param {boolean} [useCapture]
  713. */
  714. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  715. if (element.removeEventListener) {
  716. // non-IE browsers
  717. if (useCapture === undefined)
  718. useCapture = false;
  719. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  720. action = "DOMMouseScroll"; // For Firefox
  721. }
  722. element.removeEventListener(action, listener, useCapture);
  723. } else {
  724. // IE browsers
  725. element.detachEvent("on" + action, listener);
  726. }
  727. };
  728. /**
  729. * Get HTML element which is the target of the event
  730. * @param {Event} event
  731. * @return {Element} target element
  732. */
  733. util.getTarget = function getTarget(event) {
  734. // code from http://www.quirksmode.org/js/events_properties.html
  735. if (!event) {
  736. event = window.event;
  737. }
  738. var target;
  739. if (event.target) {
  740. target = event.target;
  741. }
  742. else if (event.srcElement) {
  743. target = event.srcElement;
  744. }
  745. if (target.nodeType != undefined && target.nodeType == 3) {
  746. // defeat Safari bug
  747. target = target.parentNode;
  748. }
  749. return target;
  750. };
  751. /**
  752. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  753. * @param {Element} element
  754. * @param {Event} event
  755. */
  756. util.fakeGesture = function fakeGesture (element, event) {
  757. var eventType = null;
  758. // for hammer.js 1.0.5
  759. var gesture = Hammer.event.collectEventData(this, eventType, event);
  760. // for hammer.js 1.0.6
  761. //var touches = Hammer.event.getTouchList(event, eventType);
  762. // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
  763. // on IE in standards mode, no touches are recognized by hammer.js,
  764. // resulting in NaN values for center.pageX and center.pageY
  765. if (isNaN(gesture.center.pageX)) {
  766. gesture.center.pageX = event.pageX;
  767. }
  768. if (isNaN(gesture.center.pageY)) {
  769. gesture.center.pageY = event.pageY;
  770. }
  771. return gesture;
  772. };
  773. util.option = {};
  774. /**
  775. * Convert a value into a boolean
  776. * @param {Boolean | function | undefined} value
  777. * @param {Boolean} [defaultValue]
  778. * @returns {Boolean} bool
  779. */
  780. util.option.asBoolean = function (value, defaultValue) {
  781. if (typeof value == 'function') {
  782. value = value();
  783. }
  784. if (value != null) {
  785. return (value != false);
  786. }
  787. return defaultValue || null;
  788. };
  789. /**
  790. * Convert a value into a number
  791. * @param {Boolean | function | undefined} value
  792. * @param {Number} [defaultValue]
  793. * @returns {Number} number
  794. */
  795. util.option.asNumber = function (value, defaultValue) {
  796. if (typeof value == 'function') {
  797. value = value();
  798. }
  799. if (value != null) {
  800. return Number(value) || defaultValue || null;
  801. }
  802. return defaultValue || null;
  803. };
  804. /**
  805. * Convert a value into a string
  806. * @param {String | function | undefined} value
  807. * @param {String} [defaultValue]
  808. * @returns {String} str
  809. */
  810. util.option.asString = function (value, defaultValue) {
  811. if (typeof value == 'function') {
  812. value = value();
  813. }
  814. if (value != null) {
  815. return String(value);
  816. }
  817. return defaultValue || null;
  818. };
  819. /**
  820. * Convert a size or location into a string with pixels or a percentage
  821. * @param {String | Number | function | undefined} value
  822. * @param {String} [defaultValue]
  823. * @returns {String} size
  824. */
  825. util.option.asSize = function (value, defaultValue) {
  826. if (typeof value == 'function') {
  827. value = value();
  828. }
  829. if (util.isString(value)) {
  830. return value;
  831. }
  832. else if (util.isNumber(value)) {
  833. return value + 'px';
  834. }
  835. else {
  836. return defaultValue || null;
  837. }
  838. };
  839. /**
  840. * Convert a value into a DOM element
  841. * @param {HTMLElement | function | undefined} value
  842. * @param {HTMLElement} [defaultValue]
  843. * @returns {HTMLElement | null} dom
  844. */
  845. util.option.asElement = function (value, defaultValue) {
  846. if (typeof value == 'function') {
  847. value = value();
  848. }
  849. return value || defaultValue || null;
  850. };
  851. util.GiveDec = function GiveDec(Hex) {
  852. var Value;
  853. if (Hex == "A")
  854. Value = 10;
  855. else if (Hex == "B")
  856. Value = 11;
  857. else if (Hex == "C")
  858. Value = 12;
  859. else if (Hex == "D")
  860. Value = 13;
  861. else if (Hex == "E")
  862. Value = 14;
  863. else if (Hex == "F")
  864. Value = 15;
  865. else
  866. Value = eval(Hex);
  867. return Value;
  868. };
  869. util.GiveHex = function GiveHex(Dec) {
  870. var Value;
  871. if(Dec == 10)
  872. Value = "A";
  873. else if (Dec == 11)
  874. Value = "B";
  875. else if (Dec == 12)
  876. Value = "C";
  877. else if (Dec == 13)
  878. Value = "D";
  879. else if (Dec == 14)
  880. Value = "E";
  881. else if (Dec == 15)
  882. Value = "F";
  883. else
  884. Value = "" + Dec;
  885. return Value;
  886. };
  887. /**
  888. * Parse a color property into an object with border, background, and
  889. * highlight colors
  890. * @param {Object | String} color
  891. * @return {Object} colorObject
  892. */
  893. util.parseColor = function(color) {
  894. var c;
  895. if (util.isString(color)) {
  896. if (util.isValidHex(color)) {
  897. var hsv = util.hexToHSV(color);
  898. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  899. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  900. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  901. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  902. c = {
  903. background: color,
  904. border:darkerColorHex,
  905. highlight: {
  906. background:lighterColorHex,
  907. border:darkerColorHex
  908. }
  909. };
  910. }
  911. else {
  912. c = {
  913. background:color,
  914. border:color,
  915. highlight: {
  916. background:color,
  917. border:color
  918. }
  919. };
  920. }
  921. }
  922. else {
  923. c = {};
  924. c.background = color.background || 'white';
  925. c.border = color.border || c.background;
  926. if (util.isString(color.highlight)) {
  927. c.highlight = {
  928. border: color.highlight,
  929. background: color.highlight
  930. }
  931. }
  932. else {
  933. c.highlight = {};
  934. c.highlight.background = color.highlight && color.highlight.background || c.background;
  935. c.highlight.border = color.highlight && color.highlight.border || c.border;
  936. }
  937. }
  938. return c;
  939. };
  940. /**
  941. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  942. *
  943. * @param {String} hex
  944. * @returns {{r: *, g: *, b: *}}
  945. */
  946. util.hexToRGB = function hexToRGB(hex) {
  947. hex = hex.replace("#","").toUpperCase();
  948. var a = util.GiveDec(hex.substring(0, 1));
  949. var b = util.GiveDec(hex.substring(1, 2));
  950. var c = util.GiveDec(hex.substring(2, 3));
  951. var d = util.GiveDec(hex.substring(3, 4));
  952. var e = util.GiveDec(hex.substring(4, 5));
  953. var f = util.GiveDec(hex.substring(5, 6));
  954. var r = (a * 16) + b;
  955. var g = (c * 16) + d;
  956. var b = (e * 16) + f;
  957. return {r:r,g:g,b:b};
  958. };
  959. util.RGBToHex = function RGBToHex(red,green,blue) {
  960. var a = util.GiveHex(Math.floor(red / 16));
  961. var b = util.GiveHex(red % 16);
  962. var c = util.GiveHex(Math.floor(green / 16));
  963. var d = util.GiveHex(green % 16);
  964. var e = util.GiveHex(Math.floor(blue / 16));
  965. var f = util.GiveHex(blue % 16);
  966. var hex = a + b + c + d + e + f;
  967. return "#" + hex;
  968. };
  969. /**
  970. * http://www.javascripter.net/faq/rgb2hsv.htm
  971. *
  972. * @param red
  973. * @param green
  974. * @param blue
  975. * @returns {*}
  976. * @constructor
  977. */
  978. util.RGBToHSV = function RGBToHSV (red,green,blue) {
  979. red=red/255; green=green/255; blue=blue/255;
  980. var minRGB = Math.min(red,Math.min(green,blue));
  981. var maxRGB = Math.max(red,Math.max(green,blue));
  982. // Black-gray-white
  983. if (minRGB == maxRGB) {
  984. return {h:0,s:0,v:minRGB};
  985. }
  986. // Colors other than black-gray-white:
  987. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  988. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  989. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  990. var saturation = (maxRGB - minRGB)/maxRGB;
  991. var value = maxRGB;
  992. return {h:hue,s:saturation,v:value};
  993. };
  994. /**
  995. * https://gist.github.com/mjijackson/5311256
  996. * @param hue
  997. * @param saturation
  998. * @param value
  999. * @returns {{r: number, g: number, b: number}}
  1000. * @constructor
  1001. */
  1002. util.HSVToRGB = function HSVToRGB(h, s, v) {
  1003. var r, g, b;
  1004. var i = Math.floor(h * 6);
  1005. var f = h * 6 - i;
  1006. var p = v * (1 - s);
  1007. var q = v * (1 - f * s);
  1008. var t = v * (1 - (1 - f) * s);
  1009. switch (i % 6) {
  1010. case 0: r = v, g = t, b = p; break;
  1011. case 1: r = q, g = v, b = p; break;
  1012. case 2: r = p, g = v, b = t; break;
  1013. case 3: r = p, g = q, b = v; break;
  1014. case 4: r = t, g = p, b = v; break;
  1015. case 5: r = v, g = p, b = q; break;
  1016. }
  1017. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  1018. };
  1019. util.HSVToHex = function HSVToHex(h, s, v) {
  1020. var rgb = util.HSVToRGB(h, s, v);
  1021. return util.RGBToHex(rgb.r, rgb.g, rgb.b);
  1022. };
  1023. util.hexToHSV = function hexToHSV(hex) {
  1024. var rgb = util.hexToRGB(hex);
  1025. return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1026. };
  1027. util.isValidHex = function isValidHex(hex) {
  1028. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1029. return isOk;
  1030. };
  1031. util.copyObject = function copyObject(objectFrom, objectTo) {
  1032. for (var i in objectFrom) {
  1033. if (objectFrom.hasOwnProperty(i)) {
  1034. if (typeof objectFrom[i] == "object") {
  1035. objectTo[i] = {};
  1036. util.copyObject(objectFrom[i], objectTo[i]);
  1037. }
  1038. else {
  1039. objectTo[i] = objectFrom[i];
  1040. }
  1041. }
  1042. }
  1043. };
  1044. /**
  1045. * DataSet
  1046. *
  1047. * Usage:
  1048. * var dataSet = new DataSet({
  1049. * fieldId: '_id',
  1050. * convert: {
  1051. * // ...
  1052. * }
  1053. * });
  1054. *
  1055. * dataSet.add(item);
  1056. * dataSet.add(data);
  1057. * dataSet.update(item);
  1058. * dataSet.update(data);
  1059. * dataSet.remove(id);
  1060. * dataSet.remove(ids);
  1061. * var data = dataSet.get();
  1062. * var data = dataSet.get(id);
  1063. * var data = dataSet.get(ids);
  1064. * var data = dataSet.get(ids, options, data);
  1065. * dataSet.clear();
  1066. *
  1067. * A data set can:
  1068. * - add/remove/update data
  1069. * - gives triggers upon changes in the data
  1070. * - can import/export data in various data formats
  1071. *
  1072. * @param {Array | DataTable} [data] Optional array with initial data
  1073. * @param {Object} [options] Available options:
  1074. * {String} fieldId Field name of the id in the
  1075. * items, 'id' by default.
  1076. * {Object.<String, String} convert
  1077. * A map with field names as key,
  1078. * and the field type as value.
  1079. * @constructor DataSet
  1080. */
  1081. // TODO: add a DataSet constructor DataSet(data, options)
  1082. function DataSet (data, options) {
  1083. this.id = util.randomUUID();
  1084. // correctly read optional arguments
  1085. if (data && !Array.isArray(data) && !util.isDataTable(data)) {
  1086. options = data;
  1087. data = null;
  1088. }
  1089. this.options = options || {};
  1090. this.data = {}; // map with data indexed by id
  1091. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1092. this.convert = {}; // field types by field name
  1093. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1094. if (this.options.convert) {
  1095. for (var field in this.options.convert) {
  1096. if (this.options.convert.hasOwnProperty(field)) {
  1097. var value = this.options.convert[field];
  1098. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1099. this.convert[field] = 'Date';
  1100. }
  1101. else {
  1102. this.convert[field] = value;
  1103. }
  1104. }
  1105. }
  1106. }
  1107. this.subscribers = {}; // event subscribers
  1108. this.internalIds = {}; // internally generated id's
  1109. // add initial data when provided
  1110. if (data) {
  1111. this.add(data);
  1112. }
  1113. }
  1114. /**
  1115. * Subscribe to an event, add an event listener
  1116. * @param {String} event Event name. Available events: 'put', 'update',
  1117. * 'remove'
  1118. * @param {function} callback Callback method. Called with three parameters:
  1119. * {String} event
  1120. * {Object | null} params
  1121. * {String | Number} senderId
  1122. */
  1123. DataSet.prototype.on = function on (event, callback) {
  1124. var subscribers = this.subscribers[event];
  1125. if (!subscribers) {
  1126. subscribers = [];
  1127. this.subscribers[event] = subscribers;
  1128. }
  1129. subscribers.push({
  1130. callback: callback
  1131. });
  1132. };
  1133. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1134. DataSet.prototype.subscribe = DataSet.prototype.on;
  1135. /**
  1136. * Unsubscribe from an event, remove an event listener
  1137. * @param {String} event
  1138. * @param {function} callback
  1139. */
  1140. DataSet.prototype.off = function off(event, callback) {
  1141. var subscribers = this.subscribers[event];
  1142. if (subscribers) {
  1143. this.subscribers[event] = subscribers.filter(function (listener) {
  1144. return (listener.callback != callback);
  1145. });
  1146. }
  1147. };
  1148. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1149. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1150. /**
  1151. * Trigger an event
  1152. * @param {String} event
  1153. * @param {Object | null} params
  1154. * @param {String} [senderId] Optional id of the sender.
  1155. * @private
  1156. */
  1157. DataSet.prototype._trigger = function (event, params, senderId) {
  1158. if (event == '*') {
  1159. throw new Error('Cannot trigger event *');
  1160. }
  1161. var subscribers = [];
  1162. if (event in this.subscribers) {
  1163. subscribers = subscribers.concat(this.subscribers[event]);
  1164. }
  1165. if ('*' in this.subscribers) {
  1166. subscribers = subscribers.concat(this.subscribers['*']);
  1167. }
  1168. for (var i = 0; i < subscribers.length; i++) {
  1169. var subscriber = subscribers[i];
  1170. if (subscriber.callback) {
  1171. subscriber.callback(event, params, senderId || null);
  1172. }
  1173. }
  1174. };
  1175. /**
  1176. * Add data.
  1177. * Adding an item will fail when there already is an item with the same id.
  1178. * @param {Object | Array | DataTable} data
  1179. * @param {String} [senderId] Optional sender id
  1180. * @return {Array} addedIds Array with the ids of the added items
  1181. */
  1182. DataSet.prototype.add = function (data, senderId) {
  1183. var addedIds = [],
  1184. id,
  1185. me = this;
  1186. if (data instanceof Array) {
  1187. // Array
  1188. for (var i = 0, len = data.length; i < len; i++) {
  1189. id = me._addItem(data[i]);
  1190. addedIds.push(id);
  1191. }
  1192. }
  1193. else if (util.isDataTable(data)) {
  1194. // Google DataTable
  1195. var columns = this._getColumnNames(data);
  1196. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1197. var item = {};
  1198. for (var col = 0, cols = columns.length; col < cols; col++) {
  1199. var field = columns[col];
  1200. item[field] = data.getValue(row, col);
  1201. }
  1202. id = me._addItem(item);
  1203. addedIds.push(id);
  1204. }
  1205. }
  1206. else if (data instanceof Object) {
  1207. // Single item
  1208. id = me._addItem(data);
  1209. addedIds.push(id);
  1210. }
  1211. else {
  1212. throw new Error('Unknown dataType');
  1213. }
  1214. if (addedIds.length) {
  1215. this._trigger('add', {items: addedIds}, senderId);
  1216. }
  1217. return addedIds;
  1218. };
  1219. /**
  1220. * Update existing items. When an item does not exist, it will be created
  1221. * @param {Object | Array | DataTable} data
  1222. * @param {String} [senderId] Optional sender id
  1223. * @return {Array} updatedIds The ids of the added or updated items
  1224. */
  1225. DataSet.prototype.update = function (data, senderId) {
  1226. var addedIds = [],
  1227. updatedIds = [],
  1228. me = this,
  1229. fieldId = me.fieldId;
  1230. var addOrUpdate = function (item) {
  1231. var id = item[fieldId];
  1232. if (me.data[id]) {
  1233. // update item
  1234. id = me._updateItem(item);
  1235. updatedIds.push(id);
  1236. }
  1237. else {
  1238. // add new item
  1239. id = me._addItem(item);
  1240. addedIds.push(id);
  1241. }
  1242. };
  1243. if (data instanceof Array) {
  1244. // Array
  1245. for (var i = 0, len = data.length; i < len; i++) {
  1246. addOrUpdate(data[i]);
  1247. }
  1248. }
  1249. else if (util.isDataTable(data)) {
  1250. // Google DataTable
  1251. var columns = this._getColumnNames(data);
  1252. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1253. var item = {};
  1254. for (var col = 0, cols = columns.length; col < cols; col++) {
  1255. var field = columns[col];
  1256. item[field] = data.getValue(row, col);
  1257. }
  1258. addOrUpdate(item);
  1259. }
  1260. }
  1261. else if (data instanceof Object) {
  1262. // Single item
  1263. addOrUpdate(data);
  1264. }
  1265. else {
  1266. throw new Error('Unknown dataType');
  1267. }
  1268. if (addedIds.length) {
  1269. this._trigger('add', {items: addedIds}, senderId);
  1270. }
  1271. if (updatedIds.length) {
  1272. this._trigger('update', {items: updatedIds}, senderId);
  1273. }
  1274. return addedIds.concat(updatedIds);
  1275. };
  1276. /**
  1277. * Get a data item or multiple items.
  1278. *
  1279. * Usage:
  1280. *
  1281. * get()
  1282. * get(options: Object)
  1283. * get(options: Object, data: Array | DataTable)
  1284. *
  1285. * get(id: Number | String)
  1286. * get(id: Number | String, options: Object)
  1287. * get(id: Number | String, options: Object, data: Array | DataTable)
  1288. *
  1289. * get(ids: Number[] | String[])
  1290. * get(ids: Number[] | String[], options: Object)
  1291. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1292. *
  1293. * Where:
  1294. *
  1295. * {Number | String} id The id of an item
  1296. * {Number[] | String{}} ids An array with ids of items
  1297. * {Object} options An Object with options. Available options:
  1298. * {String} [type] Type of data to be returned. Can
  1299. * be 'DataTable' or 'Array' (default)
  1300. * {Object.<String, String>} [convert]
  1301. * {String[]} [fields] field names to be returned
  1302. * {function} [filter] filter items
  1303. * {String | function} [order] Order the items by
  1304. * a field name or custom sort function.
  1305. * {Array | DataTable} [data] If provided, items will be appended to this
  1306. * array or table. Required in case of Google
  1307. * DataTable.
  1308. *
  1309. * @throws Error
  1310. */
  1311. DataSet.prototype.get = function (args) {
  1312. var me = this;
  1313. var globalShowInternalIds = this.showInternalIds;
  1314. // parse the arguments
  1315. var id, ids, options, data;
  1316. var firstType = util.getType(arguments[0]);
  1317. if (firstType == 'String' || firstType == 'Number') {
  1318. // get(id [, options] [, data])
  1319. id = arguments[0];
  1320. options = arguments[1];
  1321. data = arguments[2];
  1322. }
  1323. else if (firstType == 'Array') {
  1324. // get(ids [, options] [, data])
  1325. ids = arguments[0];
  1326. options = arguments[1];
  1327. data = arguments[2];
  1328. }
  1329. else {
  1330. // get([, options] [, data])
  1331. options = arguments[0];
  1332. data = arguments[1];
  1333. }
  1334. // determine the return type
  1335. var type;
  1336. if (options && options.type) {
  1337. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1338. if (data && (type != util.getType(data))) {
  1339. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1340. 'does not correspond with specified options.type (' + options.type + ')');
  1341. }
  1342. if (type == 'DataTable' && !util.isDataTable(data)) {
  1343. throw new Error('Parameter "data" must be a DataTable ' +
  1344. 'when options.type is "DataTable"');
  1345. }
  1346. }
  1347. else if (data) {
  1348. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1349. }
  1350. else {
  1351. type = 'Array';
  1352. }
  1353. // we allow the setting of this value for a single get request.
  1354. if (options != undefined) {
  1355. if (options.showInternalIds != undefined) {
  1356. this.showInternalIds = options.showInternalIds;
  1357. }
  1358. }
  1359. // build options
  1360. var convert = options && options.convert || this.options.convert;
  1361. var filter = options && options.filter;
  1362. var items = [], item, itemId, i, len;
  1363. // convert items
  1364. if (id != undefined) {
  1365. // return a single item
  1366. item = me._getItem(id, convert);
  1367. if (filter && !filter(item)) {
  1368. item = null;
  1369. }
  1370. }
  1371. else if (ids != undefined) {
  1372. // return a subset of items
  1373. for (i = 0, len = ids.length; i < len; i++) {
  1374. item = me._getItem(ids[i], convert);
  1375. if (!filter || filter(item)) {
  1376. items.push(item);
  1377. }
  1378. }
  1379. }
  1380. else {
  1381. // return all items
  1382. for (itemId in this.data) {
  1383. if (this.data.hasOwnProperty(itemId)) {
  1384. item = me._getItem(itemId, convert);
  1385. if (!filter || filter(item)) {
  1386. items.push(item);
  1387. }
  1388. }
  1389. }
  1390. }
  1391. // restore the global value of showInternalIds
  1392. this.showInternalIds = globalShowInternalIds;
  1393. // order the results
  1394. if (options && options.order && id == undefined) {
  1395. this._sort(items, options.order);
  1396. }
  1397. // filter fields of the items
  1398. if (options && options.fields) {
  1399. var fields = options.fields;
  1400. if (id != undefined) {
  1401. item = this._filterFields(item, fields);
  1402. }
  1403. else {
  1404. for (i = 0, len = items.length; i < len; i++) {
  1405. items[i] = this._filterFields(items[i], fields);
  1406. }
  1407. }
  1408. }
  1409. // return the results
  1410. if (type == 'DataTable') {
  1411. var columns = this._getColumnNames(data);
  1412. if (id != undefined) {
  1413. // append a single item to the data table
  1414. me._appendRow(data, columns, item);
  1415. }
  1416. else {
  1417. // copy the items to the provided data table
  1418. for (i = 0, len = items.length; i < len; i++) {
  1419. me._appendRow(data, columns, items[i]);
  1420. }
  1421. }
  1422. return data;
  1423. }
  1424. else {
  1425. // return an array
  1426. if (id != undefined) {
  1427. // a single item
  1428. return item;
  1429. }
  1430. else {
  1431. // multiple items
  1432. if (data) {
  1433. // copy the items to the provided array
  1434. for (i = 0, len = items.length; i < len; i++) {
  1435. data.push(items[i]);
  1436. }
  1437. return data;
  1438. }
  1439. else {
  1440. // just return our array
  1441. return items;
  1442. }
  1443. }
  1444. }
  1445. };
  1446. /**
  1447. * Get ids of all items or from a filtered set of items.
  1448. * @param {Object} [options] An Object with options. Available options:
  1449. * {function} [filter] filter items
  1450. * {String | function} [order] Order the items by
  1451. * a field name or custom sort function.
  1452. * @return {Array} ids
  1453. */
  1454. DataSet.prototype.getIds = function (options) {
  1455. var data = this.data,
  1456. filter = options && options.filter,
  1457. order = options && options.order,
  1458. convert = options && options.convert || this.options.convert,
  1459. i,
  1460. len,
  1461. id,
  1462. item,
  1463. items,
  1464. ids = [];
  1465. if (filter) {
  1466. // get filtered items
  1467. if (order) {
  1468. // create ordered list
  1469. items = [];
  1470. for (id in data) {
  1471. if (data.hasOwnProperty(id)) {
  1472. item = this._getItem(id, convert);
  1473. if (filter(item)) {
  1474. items.push(item);
  1475. }
  1476. }
  1477. }
  1478. this._sort(items, order);
  1479. for (i = 0, len = items.length; i < len; i++) {
  1480. ids[i] = items[i][this.fieldId];
  1481. }
  1482. }
  1483. else {
  1484. // create unordered list
  1485. for (id in data) {
  1486. if (data.hasOwnProperty(id)) {
  1487. item = this._getItem(id, convert);
  1488. if (filter(item)) {
  1489. ids.push(item[this.fieldId]);
  1490. }
  1491. }
  1492. }
  1493. }
  1494. }
  1495. else {
  1496. // get all items
  1497. if (order) {
  1498. // create an ordered list
  1499. items = [];
  1500. for (id in data) {
  1501. if (data.hasOwnProperty(id)) {
  1502. items.push(data[id]);
  1503. }
  1504. }
  1505. this._sort(items, order);
  1506. for (i = 0, len = items.length; i < len; i++) {
  1507. ids[i] = items[i][this.fieldId];
  1508. }
  1509. }
  1510. else {
  1511. // create unordered list
  1512. for (id in data) {
  1513. if (data.hasOwnProperty(id)) {
  1514. item = data[id];
  1515. ids.push(item[this.fieldId]);
  1516. }
  1517. }
  1518. }
  1519. }
  1520. return ids;
  1521. };
  1522. /**
  1523. * Execute a callback function for every item in the dataset.
  1524. * The order of the items is not determined.
  1525. * @param {function} callback
  1526. * @param {Object} [options] Available options:
  1527. * {Object.<String, String>} [convert]
  1528. * {String[]} [fields] filter fields
  1529. * {function} [filter] filter items
  1530. * {String | function} [order] Order the items by
  1531. * a field name or custom sort function.
  1532. */
  1533. DataSet.prototype.forEach = function (callback, options) {
  1534. var filter = options && options.filter,
  1535. convert = options && options.convert || this.options.convert,
  1536. data = this.data,
  1537. item,
  1538. id;
  1539. if (options && options.order) {
  1540. // execute forEach on ordered list
  1541. var items = this.get(options);
  1542. for (var i = 0, len = items.length; i < len; i++) {
  1543. item = items[i];
  1544. id = item[this.fieldId];
  1545. callback(item, id);
  1546. }
  1547. }
  1548. else {
  1549. // unordered
  1550. for (id in data) {
  1551. if (data.hasOwnProperty(id)) {
  1552. item = this._getItem(id, convert);
  1553. if (!filter || filter(item)) {
  1554. callback(item, id);
  1555. }
  1556. }
  1557. }
  1558. }
  1559. };
  1560. /**
  1561. * Map every item in the dataset.
  1562. * @param {function} callback
  1563. * @param {Object} [options] Available options:
  1564. * {Object.<String, String>} [convert]
  1565. * {String[]} [fields] filter fields
  1566. * {function} [filter] filter items
  1567. * {String | function} [order] Order the items by
  1568. * a field name or custom sort function.
  1569. * @return {Object[]} mappedItems
  1570. */
  1571. DataSet.prototype.map = function (callback, options) {
  1572. var filter = options && options.filter,
  1573. convert = options && options.convert || this.options.convert,
  1574. mappedItems = [],
  1575. data = this.data,
  1576. item;
  1577. // convert and filter items
  1578. for (var id in data) {
  1579. if (data.hasOwnProperty(id)) {
  1580. item = this._getItem(id, convert);
  1581. if (!filter || filter(item)) {
  1582. mappedItems.push(callback(item, id));
  1583. }
  1584. }
  1585. }
  1586. // order items
  1587. if (options && options.order) {
  1588. this._sort(mappedItems, options.order);
  1589. }
  1590. return mappedItems;
  1591. };
  1592. /**
  1593. * Filter the fields of an item
  1594. * @param {Object} item
  1595. * @param {String[]} fields Field names
  1596. * @return {Object} filteredItem
  1597. * @private
  1598. */
  1599. DataSet.prototype._filterFields = function (item, fields) {
  1600. var filteredItem = {};
  1601. for (var field in item) {
  1602. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1603. filteredItem[field] = item[field];
  1604. }
  1605. }
  1606. return filteredItem;
  1607. };
  1608. /**
  1609. * Sort the provided array with items
  1610. * @param {Object[]} items
  1611. * @param {String | function} order A field name or custom sort function.
  1612. * @private
  1613. */
  1614. DataSet.prototype._sort = function (items, order) {
  1615. if (util.isString(order)) {
  1616. // order by provided field name
  1617. var name = order; // field name
  1618. items.sort(function (a, b) {
  1619. var av = a[name];
  1620. var bv = b[name];
  1621. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1622. });
  1623. }
  1624. else if (typeof order === 'function') {
  1625. // order by sort function
  1626. items.sort(order);
  1627. }
  1628. // TODO: extend order by an Object {field:String, direction:String}
  1629. // where direction can be 'asc' or 'desc'
  1630. else {
  1631. throw new TypeError('Order must be a function or a string');
  1632. }
  1633. };
  1634. /**
  1635. * Remove an object by pointer or by id
  1636. * @param {String | Number | Object | Array} id Object or id, or an array with
  1637. * objects or ids to be removed
  1638. * @param {String} [senderId] Optional sender id
  1639. * @return {Array} removedIds
  1640. */
  1641. DataSet.prototype.remove = function (id, senderId) {
  1642. var removedIds = [],
  1643. i, len, removedId;
  1644. if (id instanceof Array) {
  1645. for (i = 0, len = id.length; i < len; i++) {
  1646. removedId = this._remove(id[i]);
  1647. if (removedId != null) {
  1648. removedIds.push(removedId);
  1649. }
  1650. }
  1651. }
  1652. else {
  1653. removedId = this._remove(id);
  1654. if (removedId != null) {
  1655. removedIds.push(removedId);
  1656. }
  1657. }
  1658. if (removedIds.length) {
  1659. this._trigger('remove', {items: removedIds}, senderId);
  1660. }
  1661. return removedIds;
  1662. };
  1663. /**
  1664. * Remove an item by its id
  1665. * @param {Number | String | Object} id id or item
  1666. * @returns {Number | String | null} id
  1667. * @private
  1668. */
  1669. DataSet.prototype._remove = function (id) {
  1670. if (util.isNumber(id) || util.isString(id)) {
  1671. if (this.data[id]) {
  1672. delete this.data[id];
  1673. delete this.internalIds[id];
  1674. return id;
  1675. }
  1676. }
  1677. else if (id instanceof Object) {
  1678. var itemId = id[this.fieldId];
  1679. if (itemId && this.data[itemId]) {
  1680. delete this.data[itemId];
  1681. delete this.internalIds[itemId];
  1682. return itemId;
  1683. }
  1684. }
  1685. return null;
  1686. };
  1687. /**
  1688. * Clear the data
  1689. * @param {String} [senderId] Optional sender id
  1690. * @return {Array} removedIds The ids of all removed items
  1691. */
  1692. DataSet.prototype.clear = function (senderId) {
  1693. var ids = Object.keys(this.data);
  1694. this.data = {};
  1695. this.internalIds = {};
  1696. this._trigger('remove', {items: ids}, senderId);
  1697. return ids;
  1698. };
  1699. /**
  1700. * Find the item with maximum value of a specified field
  1701. * @param {String} field
  1702. * @return {Object | null} item Item containing max value, or null if no items
  1703. */
  1704. DataSet.prototype.max = function (field) {
  1705. var data = this.data,
  1706. max = null,
  1707. maxField = null;
  1708. for (var id in data) {
  1709. if (data.hasOwnProperty(id)) {
  1710. var item = data[id];
  1711. var itemField = item[field];
  1712. if (itemField != null && (!max || itemField > maxField)) {
  1713. max = item;
  1714. maxField = itemField;
  1715. }
  1716. }
  1717. }
  1718. return max;
  1719. };
  1720. /**
  1721. * Find the item with minimum value of a specified field
  1722. * @param {String} field
  1723. * @return {Object | null} item Item containing max value, or null if no items
  1724. */
  1725. DataSet.prototype.min = function (field) {
  1726. var data = this.data,
  1727. min = null,
  1728. minField = null;
  1729. for (var id in data) {
  1730. if (data.hasOwnProperty(id)) {
  1731. var item = data[id];
  1732. var itemField = item[field];
  1733. if (itemField != null && (!min || itemField < minField)) {
  1734. min = item;
  1735. minField = itemField;
  1736. }
  1737. }
  1738. }
  1739. return min;
  1740. };
  1741. /**
  1742. * Find all distinct values of a specified field
  1743. * @param {String} field
  1744. * @return {Array} values Array containing all distinct values. If the data
  1745. * items do not contain the specified field, an array
  1746. * containing a single value undefined is returned.
  1747. * The returned array is unordered.
  1748. */
  1749. DataSet.prototype.distinct = function (field) {
  1750. var data = this.data,
  1751. values = [],
  1752. fieldType = this.options.convert[field],
  1753. count = 0;
  1754. for (var prop in data) {
  1755. if (data.hasOwnProperty(prop)) {
  1756. var item = data[prop];
  1757. var value = util.convert(item[field], fieldType);
  1758. var exists = false;
  1759. for (var i = 0; i < count; i++) {
  1760. if (values[i] == value) {
  1761. exists = true;
  1762. break;
  1763. }
  1764. }
  1765. if (!exists) {
  1766. values[count] = value;
  1767. count++;
  1768. }
  1769. }
  1770. }
  1771. return values;
  1772. };
  1773. /**
  1774. * Add a single item. Will fail when an item with the same id already exists.
  1775. * @param {Object} item
  1776. * @return {String} id
  1777. * @private
  1778. */
  1779. DataSet.prototype._addItem = function (item) {
  1780. var id = item[this.fieldId];
  1781. if (id != undefined) {
  1782. // check whether this id is already taken
  1783. if (this.data[id]) {
  1784. // item already exists
  1785. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1786. }
  1787. }
  1788. else {
  1789. // generate an id
  1790. id = util.randomUUID();
  1791. item[this.fieldId] = id;
  1792. this.internalIds[id] = item;
  1793. }
  1794. var d = {};
  1795. for (var field in item) {
  1796. if (item.hasOwnProperty(field)) {
  1797. var fieldType = this.convert[field]; // type may be undefined
  1798. d[field] = util.convert(item[field], fieldType);
  1799. }
  1800. }
  1801. this.data[id] = d;
  1802. return id;
  1803. };
  1804. /**
  1805. * Get an item. Fields can be converted to a specific type
  1806. * @param {String} id
  1807. * @param {Object.<String, String>} [convert] field types to convert
  1808. * @return {Object | null} item
  1809. * @private
  1810. */
  1811. DataSet.prototype._getItem = function (id, convert) {
  1812. var field, value;
  1813. // get the item from the dataset
  1814. var raw = this.data[id];
  1815. if (!raw) {
  1816. return null;
  1817. }
  1818. // convert the items field types
  1819. var converted = {},
  1820. fieldId = this.fieldId,
  1821. internalIds = this.internalIds;
  1822. if (convert) {
  1823. for (field in raw) {
  1824. if (raw.hasOwnProperty(field)) {
  1825. value = raw[field];
  1826. // output all fields, except internal ids
  1827. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1828. converted[field] = util.convert(value, convert[field]);
  1829. }
  1830. }
  1831. }
  1832. }
  1833. else {
  1834. // no field types specified, no converting needed
  1835. for (field in raw) {
  1836. if (raw.hasOwnProperty(field)) {
  1837. value = raw[field];
  1838. // output all fields, except internal ids
  1839. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1840. converted[field] = value;
  1841. }
  1842. }
  1843. }
  1844. }
  1845. return converted;
  1846. };
  1847. /**
  1848. * Update a single item: merge with existing item.
  1849. * Will fail when the item has no id, or when there does not exist an item
  1850. * with the same id.
  1851. * @param {Object} item
  1852. * @return {String} id
  1853. * @private
  1854. */
  1855. DataSet.prototype._updateItem = function (item) {
  1856. var id = item[this.fieldId];
  1857. if (id == undefined) {
  1858. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1859. }
  1860. var d = this.data[id];
  1861. if (!d) {
  1862. // item doesn't exist
  1863. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1864. }
  1865. // merge with current item
  1866. for (var field in item) {
  1867. if (item.hasOwnProperty(field)) {
  1868. var fieldType = this.convert[field]; // type may be undefined
  1869. d[field] = util.convert(item[field], fieldType);
  1870. }
  1871. }
  1872. return id;
  1873. };
  1874. /**
  1875. * check if an id is an internal or external id
  1876. * @param id
  1877. * @returns {boolean}
  1878. * @private
  1879. */
  1880. DataSet.prototype.isInternalId = function(id) {
  1881. return (id in this.internalIds);
  1882. };
  1883. /**
  1884. * Get an array with the column names of a Google DataTable
  1885. * @param {DataTable} dataTable
  1886. * @return {String[]} columnNames
  1887. * @private
  1888. */
  1889. DataSet.prototype._getColumnNames = function (dataTable) {
  1890. var columns = [];
  1891. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1892. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1893. }
  1894. return columns;
  1895. };
  1896. /**
  1897. * Append an item as a row to the dataTable
  1898. * @param dataTable
  1899. * @param columns
  1900. * @param item
  1901. * @private
  1902. */
  1903. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1904. var row = dataTable.addRow();
  1905. for (var col = 0, cols = columns.length; col < cols; col++) {
  1906. var field = columns[col];
  1907. dataTable.setValue(row, col, item[field]);
  1908. }
  1909. };
  1910. /**
  1911. * DataView
  1912. *
  1913. * a dataview offers a filtered view on a dataset or an other dataview.
  1914. *
  1915. * @param {DataSet | DataView} data
  1916. * @param {Object} [options] Available options: see method get
  1917. *
  1918. * @constructor DataView
  1919. */
  1920. function DataView (data, options) {
  1921. this.id = util.randomUUID();
  1922. this.data = null;
  1923. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1924. this.options = options || {};
  1925. this.fieldId = 'id'; // name of the field containing id
  1926. this.subscribers = {}; // event subscribers
  1927. var me = this;
  1928. this.listener = function () {
  1929. me._onEvent.apply(me, arguments);
  1930. };
  1931. this.setData(data);
  1932. }
  1933. // TODO: implement a function .config() to dynamically update things like configured filter
  1934. // and trigger changes accordingly
  1935. /**
  1936. * Set a data source for the view
  1937. * @param {DataSet | DataView} data
  1938. */
  1939. DataView.prototype.setData = function (data) {
  1940. var ids, dataItems, i, len;
  1941. if (this.data) {
  1942. // unsubscribe from current dataset
  1943. if (this.data.unsubscribe) {
  1944. this.data.unsubscribe('*', this.listener);
  1945. }
  1946. // trigger a remove of all items in memory
  1947. ids = [];
  1948. for (var id in this.ids) {
  1949. if (this.ids.hasOwnProperty(id)) {
  1950. ids.push(id);
  1951. }
  1952. }
  1953. this.ids = {};
  1954. this._trigger('remove', {items: ids});
  1955. }
  1956. this.data = data;
  1957. if (this.data) {
  1958. // update fieldId
  1959. this.fieldId = this.options.fieldId ||
  1960. (this.data && this.data.options && this.data.options.fieldId) ||
  1961. 'id';
  1962. // trigger an add of all added items
  1963. ids = this.data.getIds({filter: this.options && this.options.filter});
  1964. for (i = 0, len = ids.length; i < len; i++) {
  1965. id = ids[i];
  1966. this.ids[id] = true;
  1967. }
  1968. this._trigger('add', {items: ids});
  1969. // subscribe to new dataset
  1970. if (this.data.on) {
  1971. this.data.on('*', this.listener);
  1972. }
  1973. }
  1974. };
  1975. /**
  1976. * Get data from the data view
  1977. *
  1978. * Usage:
  1979. *
  1980. * get()
  1981. * get(options: Object)
  1982. * get(options: Object, data: Array | DataTable)
  1983. *
  1984. * get(id: Number)
  1985. * get(id: Number, options: Object)
  1986. * get(id: Number, options: Object, data: Array | DataTable)
  1987. *
  1988. * get(ids: Number[])
  1989. * get(ids: Number[], options: Object)
  1990. * get(ids: Number[], options: Object, data: Array | DataTable)
  1991. *
  1992. * Where:
  1993. *
  1994. * {Number | String} id The id of an item
  1995. * {Number[] | String{}} ids An array with ids of items
  1996. * {Object} options An Object with options. Available options:
  1997. * {String} [type] Type of data to be returned. Can
  1998. * be 'DataTable' or 'Array' (default)
  1999. * {Object.<String, String>} [convert]
  2000. * {String[]} [fields] field names to be returned
  2001. * {function} [filter] filter items
  2002. * {String | function} [order] Order the items by
  2003. * a field name or custom sort function.
  2004. * {Array | DataTable} [data] If provided, items will be appended to this
  2005. * array or table. Required in case of Google
  2006. * DataTable.
  2007. * @param args
  2008. */
  2009. DataView.prototype.get = function (args) {
  2010. var me = this;
  2011. // parse the arguments
  2012. var ids, options, data;
  2013. var firstType = util.getType(arguments[0]);
  2014. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2015. // get(id(s) [, options] [, data])
  2016. ids = arguments[0]; // can be a single id or an array with ids
  2017. options = arguments[1];
  2018. data = arguments[2];
  2019. }
  2020. else {
  2021. // get([, options] [, data])
  2022. options = arguments[0];
  2023. data = arguments[1];
  2024. }
  2025. // extend the options with the default options and provided options
  2026. var viewOptions = util.extend({}, this.options, options);
  2027. // create a combined filter method when needed
  2028. if (this.options.filter && options && options.filter) {
  2029. viewOptions.filter = function (item) {
  2030. return me.options.filter(item) && options.filter(item);
  2031. }
  2032. }
  2033. // build up the call to the linked data set
  2034. var getArguments = [];
  2035. if (ids != undefined) {
  2036. getArguments.push(ids);
  2037. }
  2038. getArguments.push(viewOptions);
  2039. getArguments.push(data);
  2040. return this.data && this.data.get.apply(this.data, getArguments);
  2041. };
  2042. /**
  2043. * Get ids of all items or from a filtered set of items.
  2044. * @param {Object} [options] An Object with options. Available options:
  2045. * {function} [filter] filter items
  2046. * {String | function} [order] Order the items by
  2047. * a field name or custom sort function.
  2048. * @return {Array} ids
  2049. */
  2050. DataView.prototype.getIds = function (options) {
  2051. var ids;
  2052. if (this.data) {
  2053. var defaultFilter = this.options.filter;
  2054. var filter;
  2055. if (options && options.filter) {
  2056. if (defaultFilter) {
  2057. filter = function (item) {
  2058. return defaultFilter(item) && options.filter(item);
  2059. }
  2060. }
  2061. else {
  2062. filter = options.filter;
  2063. }
  2064. }
  2065. else {
  2066. filter = defaultFilter;
  2067. }
  2068. ids = this.data.getIds({
  2069. filter: filter,
  2070. order: options && options.order
  2071. });
  2072. }
  2073. else {
  2074. ids = [];
  2075. }
  2076. return ids;
  2077. };
  2078. /**
  2079. * Event listener. Will propagate all events from the connected data set to
  2080. * the subscribers of the DataView, but will filter the items and only trigger
  2081. * when there are changes in the filtered data set.
  2082. * @param {String} event
  2083. * @param {Object | null} params
  2084. * @param {String} senderId
  2085. * @private
  2086. */
  2087. DataView.prototype._onEvent = function (event, params, senderId) {
  2088. var i, len, id, item,
  2089. ids = params && params.items,
  2090. data = this.data,
  2091. added = [],
  2092. updated = [],
  2093. removed = [];
  2094. if (ids && data) {
  2095. switch (event) {
  2096. case 'add':
  2097. // filter the ids of the added items
  2098. for (i = 0, len = ids.length; i < len; i++) {
  2099. id = ids[i];
  2100. item = this.get(id);
  2101. if (item) {
  2102. this.ids[id] = true;
  2103. added.push(id);
  2104. }
  2105. }
  2106. break;
  2107. case 'update':
  2108. // determine the event from the views viewpoint: an updated
  2109. // item can be added, updated, or removed from this view.
  2110. for (i = 0, len = ids.length; i < len; i++) {
  2111. id = ids[i];
  2112. item = this.get(id);
  2113. if (item) {
  2114. if (this.ids[id]) {
  2115. updated.push(id);
  2116. }
  2117. else {
  2118. this.ids[id] = true;
  2119. added.push(id);
  2120. }
  2121. }
  2122. else {
  2123. if (this.ids[id]) {
  2124. delete this.ids[id];
  2125. removed.push(id);
  2126. }
  2127. else {
  2128. // nothing interesting for me :-(
  2129. }
  2130. }
  2131. }
  2132. break;
  2133. case 'remove':
  2134. // filter the ids of the removed items
  2135. for (i = 0, len = ids.length; i < len; i++) {
  2136. id = ids[i];
  2137. if (this.ids[id]) {
  2138. delete this.ids[id];
  2139. removed.push(id);
  2140. }
  2141. }
  2142. break;
  2143. }
  2144. if (added.length) {
  2145. this._trigger('add', {items: added}, senderId);
  2146. }
  2147. if (updated.length) {
  2148. this._trigger('update', {items: updated}, senderId);
  2149. }
  2150. if (removed.length) {
  2151. this._trigger('remove', {items: removed}, senderId);
  2152. }
  2153. }
  2154. };
  2155. // copy subscription functionality from DataSet
  2156. DataView.prototype.on = DataSet.prototype.on;
  2157. DataView.prototype.off = DataSet.prototype.off;
  2158. DataView.prototype._trigger = DataSet.prototype._trigger;
  2159. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2160. DataView.prototype.subscribe = DataView.prototype.on;
  2161. DataView.prototype.unsubscribe = DataView.prototype.off;
  2162. /**
  2163. * @constructor TimeStep
  2164. * The class TimeStep is an iterator for dates. You provide a start date and an
  2165. * end date. The class itself determines the best scale (step size) based on the
  2166. * provided start Date, end Date, and minimumStep.
  2167. *
  2168. * If minimumStep is provided, the step size is chosen as close as possible
  2169. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2170. * provided, the scale is set to 1 DAY.
  2171. * The minimumStep should correspond with the onscreen size of about 6 characters
  2172. *
  2173. * Alternatively, you can set a scale by hand.
  2174. * After creation, you can initialize the class by executing first(). Then you
  2175. * can iterate from the start date to the end date via next(). You can check if
  2176. * the end date is reached with the function hasNext(). After each step, you can
  2177. * retrieve the current date via getCurrent().
  2178. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2179. * days, to years.
  2180. *
  2181. * Version: 1.2
  2182. *
  2183. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2184. * or new Date(2010, 9, 21, 23, 45, 00)
  2185. * @param {Date} [end] The end date
  2186. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2187. */
  2188. TimeStep = function(start, end, minimumStep) {
  2189. // variables
  2190. this.current = new Date();
  2191. this._start = new Date();
  2192. this._end = new Date();
  2193. this.autoScale = true;
  2194. this.scale = TimeStep.SCALE.DAY;
  2195. this.step = 1;
  2196. // initialize the range
  2197. this.setRange(start, end, minimumStep);
  2198. };
  2199. /// enum scale
  2200. TimeStep.SCALE = {
  2201. MILLISECOND: 1,
  2202. SECOND: 2,
  2203. MINUTE: 3,
  2204. HOUR: 4,
  2205. DAY: 5,
  2206. WEEKDAY: 6,
  2207. MONTH: 7,
  2208. YEAR: 8
  2209. };
  2210. /**
  2211. * Set a new range
  2212. * If minimumStep is provided, the step size is chosen as close as possible
  2213. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2214. * provided, the scale is set to 1 DAY.
  2215. * The minimumStep should correspond with the onscreen size of about 6 characters
  2216. * @param {Date} [start] The start date and time.
  2217. * @param {Date} [end] The end date and time.
  2218. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2219. */
  2220. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2221. if (!(start instanceof Date) || !(end instanceof Date)) {
  2222. throw "No legal start or end date in method setRange";
  2223. }
  2224. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2225. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2226. if (this.autoScale) {
  2227. this.setMinimumStep(minimumStep);
  2228. }
  2229. };
  2230. /**
  2231. * Set the range iterator to the start date.
  2232. */
  2233. TimeStep.prototype.first = function() {
  2234. this.current = new Date(this._start.valueOf());
  2235. this.roundToMinor();
  2236. };
  2237. /**
  2238. * Round the current date to the first minor date value
  2239. * This must be executed once when the current date is set to start Date
  2240. */
  2241. TimeStep.prototype.roundToMinor = function() {
  2242. // round to floor
  2243. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2244. //noinspection FallthroughInSwitchStatementJS
  2245. switch (this.scale) {
  2246. case TimeStep.SCALE.YEAR:
  2247. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2248. this.current.setMonth(0);
  2249. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2250. case TimeStep.SCALE.DAY: // intentional fall through
  2251. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2252. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2253. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2254. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2255. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2256. }
  2257. if (this.step != 1) {
  2258. // round down to the first minor value that is a multiple of the current step size
  2259. switch (this.scale) {
  2260. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2261. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2262. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2263. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2264. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2265. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2266. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2267. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2268. default: break;
  2269. }
  2270. }
  2271. };
  2272. /**
  2273. * Check if the there is a next step
  2274. * @return {boolean} true if the current date has not passed the end date
  2275. */
  2276. TimeStep.prototype.hasNext = function () {
  2277. return (this.current.valueOf() <= this._end.valueOf());
  2278. };
  2279. /**
  2280. * Do the next step
  2281. */
  2282. TimeStep.prototype.next = function() {
  2283. var prev = this.current.valueOf();
  2284. // Two cases, needed to prevent issues with switching daylight savings
  2285. // (end of March and end of October)
  2286. if (this.current.getMonth() < 6) {
  2287. switch (this.scale) {
  2288. case TimeStep.SCALE.MILLISECOND:
  2289. this.current = new Date(this.current.valueOf() + this.step); break;
  2290. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2291. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2292. case TimeStep.SCALE.HOUR:
  2293. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2294. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2295. var h = this.current.getHours();
  2296. this.current.setHours(h - (h % this.step));
  2297. break;
  2298. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2299. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2300. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2301. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2302. default: break;
  2303. }
  2304. }
  2305. else {
  2306. switch (this.scale) {
  2307. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2308. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2309. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2310. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2311. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2312. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2313. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2314. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2315. default: break;
  2316. }
  2317. }
  2318. if (this.step != 1) {
  2319. // round down to the correct major value
  2320. switch (this.scale) {
  2321. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2322. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2323. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2324. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2325. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2326. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2327. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2328. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2329. default: break;
  2330. }
  2331. }
  2332. // safety mechanism: if current time is still unchanged, move to the end
  2333. if (this.current.valueOf() == prev) {
  2334. this.current = new Date(this._end.valueOf());
  2335. }
  2336. };
  2337. /**
  2338. * Get the current datetime
  2339. * @return {Date} current The current date
  2340. */
  2341. TimeStep.prototype.getCurrent = function() {
  2342. return this.current;
  2343. };
  2344. /**
  2345. * Set a custom scale. Autoscaling will be disabled.
  2346. * For example setScale(SCALE.MINUTES, 5) will result
  2347. * in minor steps of 5 minutes, and major steps of an hour.
  2348. *
  2349. * @param {TimeStep.SCALE} newScale
  2350. * A scale. Choose from SCALE.MILLISECOND,
  2351. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2352. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2353. * SCALE.YEAR.
  2354. * @param {Number} newStep A step size, by default 1. Choose for
  2355. * example 1, 2, 5, or 10.
  2356. */
  2357. TimeStep.prototype.setScale = function(newScale, newStep) {
  2358. this.scale = newScale;
  2359. if (newStep > 0) {
  2360. this.step = newStep;
  2361. }
  2362. this.autoScale = false;
  2363. };
  2364. /**
  2365. * Enable or disable autoscaling
  2366. * @param {boolean} enable If true, autoascaling is set true
  2367. */
  2368. TimeStep.prototype.setAutoScale = function (enable) {
  2369. this.autoScale = enable;
  2370. };
  2371. /**
  2372. * Automatically determine the scale that bests fits the provided minimum step
  2373. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2374. */
  2375. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2376. if (minimumStep == undefined) {
  2377. return;
  2378. }
  2379. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2380. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2381. var stepDay = (1000 * 60 * 60 * 24);
  2382. var stepHour = (1000 * 60 * 60);
  2383. var stepMinute = (1000 * 60);
  2384. var stepSecond = (1000);
  2385. var stepMillisecond= (1);
  2386. // find the smallest step that is larger than the provided minimumStep
  2387. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2388. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2389. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2390. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2391. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2392. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2393. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2394. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2395. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2396. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2397. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2398. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2399. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2400. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2401. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2402. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2403. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2404. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2405. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2406. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2407. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2408. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2409. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2410. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2411. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2412. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2413. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2414. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2415. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2416. };
  2417. /**
  2418. * Snap a date to a rounded value.
  2419. * The snap intervals are dependent on the current scale and step.
  2420. * @param {Date} date the date to be snapped.
  2421. * @return {Date} snappedDate
  2422. */
  2423. TimeStep.prototype.snap = function(date) {
  2424. var clone = new Date(date.valueOf());
  2425. if (this.scale == TimeStep.SCALE.YEAR) {
  2426. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2427. clone.setFullYear(Math.round(year / this.step) * this.step);
  2428. clone.setMonth(0);
  2429. clone.setDate(0);
  2430. clone.setHours(0);
  2431. clone.setMinutes(0);
  2432. clone.setSeconds(0);
  2433. clone.setMilliseconds(0);
  2434. }
  2435. else if (this.scale == TimeStep.SCALE.MONTH) {
  2436. if (clone.getDate() > 15) {
  2437. clone.setDate(1);
  2438. clone.setMonth(clone.getMonth() + 1);
  2439. // important: first set Date to 1, after that change the month.
  2440. }
  2441. else {
  2442. clone.setDate(1);
  2443. }
  2444. clone.setHours(0);
  2445. clone.setMinutes(0);
  2446. clone.setSeconds(0);
  2447. clone.setMilliseconds(0);
  2448. }
  2449. else if (this.scale == TimeStep.SCALE.DAY ||
  2450. this.scale == TimeStep.SCALE.WEEKDAY) {
  2451. //noinspection FallthroughInSwitchStatementJS
  2452. switch (this.step) {
  2453. case 5:
  2454. case 2:
  2455. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2456. default:
  2457. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2458. }
  2459. clone.setMinutes(0);
  2460. clone.setSeconds(0);
  2461. clone.setMilliseconds(0);
  2462. }
  2463. else if (this.scale == TimeStep.SCALE.HOUR) {
  2464. switch (this.step) {
  2465. case 4:
  2466. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2467. default:
  2468. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2469. }
  2470. clone.setSeconds(0);
  2471. clone.setMilliseconds(0);
  2472. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2473. //noinspection FallthroughInSwitchStatementJS
  2474. switch (this.step) {
  2475. case 15:
  2476. case 10:
  2477. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2478. clone.setSeconds(0);
  2479. break;
  2480. case 5:
  2481. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2482. default:
  2483. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2484. }
  2485. clone.setMilliseconds(0);
  2486. }
  2487. else if (this.scale == TimeStep.SCALE.SECOND) {
  2488. //noinspection FallthroughInSwitchStatementJS
  2489. switch (this.step) {
  2490. case 15:
  2491. case 10:
  2492. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2493. clone.setMilliseconds(0);
  2494. break;
  2495. case 5:
  2496. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2497. default:
  2498. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2499. }
  2500. }
  2501. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2502. var step = this.step > 5 ? this.step / 2 : 1;
  2503. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2504. }
  2505. return clone;
  2506. };
  2507. /**
  2508. * Check if the current value is a major value (for example when the step
  2509. * is DAY, a major value is each first day of the MONTH)
  2510. * @return {boolean} true if current date is major, else false.
  2511. */
  2512. TimeStep.prototype.isMajor = function() {
  2513. switch (this.scale) {
  2514. case TimeStep.SCALE.MILLISECOND:
  2515. return (this.current.getMilliseconds() == 0);
  2516. case TimeStep.SCALE.SECOND:
  2517. return (this.current.getSeconds() == 0);
  2518. case TimeStep.SCALE.MINUTE:
  2519. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2520. // Note: this is no bug. Major label is equal for both minute and hour scale
  2521. case TimeStep.SCALE.HOUR:
  2522. return (this.current.getHours() == 0);
  2523. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2524. case TimeStep.SCALE.DAY:
  2525. return (this.current.getDate() == 1);
  2526. case TimeStep.SCALE.MONTH:
  2527. return (this.current.getMonth() == 0);
  2528. case TimeStep.SCALE.YEAR:
  2529. return false;
  2530. default:
  2531. return false;
  2532. }
  2533. };
  2534. /**
  2535. * Returns formatted text for the minor axislabel, depending on the current
  2536. * date and the scale. For example when scale is MINUTE, the current time is
  2537. * formatted as "hh:mm".
  2538. * @param {Date} [date] custom date. if not provided, current date is taken
  2539. */
  2540. TimeStep.prototype.getLabelMinor = function(date) {
  2541. if (date == undefined) {
  2542. date = this.current;
  2543. }
  2544. switch (this.scale) {
  2545. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2546. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2547. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2548. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2549. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2550. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2551. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2552. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2553. default: return '';
  2554. }
  2555. };
  2556. /**
  2557. * Returns formatted text for the major axis label, depending on the current
  2558. * date and the scale. For example when scale is MINUTE, the major scale is
  2559. * hours, and the hour will be formatted as "hh".
  2560. * @param {Date} [date] custom date. if not provided, current date is taken
  2561. */
  2562. TimeStep.prototype.getLabelMajor = function(date) {
  2563. if (date == undefined) {
  2564. date = this.current;
  2565. }
  2566. //noinspection FallthroughInSwitchStatementJS
  2567. switch (this.scale) {
  2568. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2569. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2570. case TimeStep.SCALE.MINUTE:
  2571. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2572. case TimeStep.SCALE.WEEKDAY:
  2573. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2574. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2575. case TimeStep.SCALE.YEAR: return '';
  2576. default: return '';
  2577. }
  2578. };
  2579. // TODO: turn Stack into a Mixin?
  2580. /**
  2581. * @constructor Stack
  2582. * Stacks items on top of each other.
  2583. * @param {Object} [options]
  2584. */
  2585. function Stack (options) {
  2586. this.options = options || {};
  2587. this.defaultOptions = {
  2588. order: function (a, b) {
  2589. // Order: ranges over non-ranges, ranged ordered by width,
  2590. // and non-ranges ordered by start.
  2591. if (a instanceof ItemRange) {
  2592. if (b instanceof ItemRange) {
  2593. var aInt = (a.data.end - a.data.start);
  2594. var bInt = (b.data.end - b.data.start);
  2595. return (aInt - bInt) || (a.data.start - b.data.start);
  2596. }
  2597. else {
  2598. return -1;
  2599. }
  2600. }
  2601. else {
  2602. if (b instanceof ItemRange) {
  2603. return 1;
  2604. }
  2605. else {
  2606. return (a.data.start - b.data.start);
  2607. }
  2608. }
  2609. },
  2610. margin: {
  2611. item: 10,
  2612. axis: 20
  2613. }
  2614. };
  2615. }
  2616. /**
  2617. * Set options for the stack
  2618. * @param {Object} options Available options:
  2619. * {Number} [margin.item=10]
  2620. * {Number} [margin.axis=20]
  2621. * {function} [order] Stacking order
  2622. */
  2623. Stack.prototype.setOptions = function setOptions (options) {
  2624. util.extend(this.options, options);
  2625. };
  2626. /**
  2627. * Order an array with items using a predefined order function for items
  2628. * @param {Item[]} items
  2629. */
  2630. Stack.prototype.order = function order(items) {
  2631. //order the items
  2632. var order = this.options.order || this.defaultOptions.order;
  2633. if (!(typeof order === 'function')) {
  2634. throw new Error('Option order must be a function');
  2635. }
  2636. items.sort(order);
  2637. };
  2638. /**
  2639. * Order items by their start data
  2640. * @param {Item[]} items
  2641. */
  2642. Stack.prototype.orderByStart = function orderByStart(items) {
  2643. items.sort(function (a, b) {
  2644. return a.data.start - b.data.start;
  2645. });
  2646. };
  2647. /**
  2648. * Order items by their end date. If they have no end date, their start date
  2649. * is used.
  2650. * @param {Item[]} items
  2651. */
  2652. Stack.prototype.orderByEnd = function orderByEnd(items) {
  2653. items.sort(function (a, b) {
  2654. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  2655. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  2656. return aTime - bTime;
  2657. });
  2658. };
  2659. /**
  2660. * Adjust vertical positions of the events such that they don't overlap each
  2661. * other.
  2662. * @param {Item[]} items All visible items
  2663. * @param {boolean} [force=false] If true, all items will be re-stacked.
  2664. * If false (default), only items having a
  2665. * top===null will be re-stacked
  2666. * @private
  2667. */
  2668. Stack.prototype.stack = function stack (items, force) {
  2669. var i,
  2670. iMax,
  2671. options = this.options,
  2672. marginItem,
  2673. marginAxis;
  2674. if (options.margin && options.margin.item !== undefined) {
  2675. marginItem = options.margin.item;
  2676. }
  2677. else {
  2678. marginItem = this.defaultOptions.margin.item
  2679. }
  2680. if (options.margin && options.margin.axis !== undefined) {
  2681. marginAxis = options.margin.axis;
  2682. }
  2683. else {
  2684. marginAxis = this.defaultOptions.margin.axis
  2685. }
  2686. if (force) {
  2687. // reset top position of all items
  2688. for (i = 0, iMax = items.length; i < iMax; i++) {
  2689. items[i].top = null;
  2690. }
  2691. }
  2692. // calculate new, non-overlapping positions
  2693. for (i = 0, iMax = items.length; i < iMax; i++) {
  2694. var item = items[i];
  2695. if (item.top === null) {
  2696. // initialize top position
  2697. item.top = marginAxis;
  2698. do {
  2699. // TODO: optimize checking for overlap. when there is a gap without items,
  2700. // you only need to check for items from the next item on, not from zero
  2701. var collidingItem = null;
  2702. for (var j = 0, jj = items.length; j < jj; j++) {
  2703. var other = items[j];
  2704. if (other.top !== null && other !== item && this.collision(item, other, marginItem)) {
  2705. collidingItem = other;
  2706. break;
  2707. }
  2708. }
  2709. if (collidingItem != null) {
  2710. // There is a collision. Reposition the event above the colliding element
  2711. item.top = collidingItem.top + collidingItem.height + marginItem;
  2712. }
  2713. } while (collidingItem);
  2714. }
  2715. }
  2716. };
  2717. /**
  2718. * Test if the two provided items collide
  2719. * The items must have parameters left, width, top, and height.
  2720. * @param {Component} a The first item
  2721. * @param {Component} b The second item
  2722. * @param {Number} margin A minimum required margin.
  2723. * If margin is provided, the two items will be
  2724. * marked colliding when they overlap or
  2725. * when the margin between the two is smaller than
  2726. * the requested margin.
  2727. * @return {boolean} true if a and b collide, else false
  2728. */
  2729. Stack.prototype.collision = function collision (a, b, margin) {
  2730. return ((a.left - margin) < (b.left + b.width) &&
  2731. (a.left + a.width + margin) > b.left &&
  2732. (a.top - margin) < (b.top + b.height) &&
  2733. (a.top + a.height + margin) > b.top);
  2734. };
  2735. /**
  2736. * @constructor Range
  2737. * A Range controls a numeric range with a start and end value.
  2738. * The Range adjusts the range based on mouse events or programmatic changes,
  2739. * and triggers events when the range is changing or has been changed.
  2740. * @param {RootPanel} root Root panel, used to subscribe to events
  2741. * @param {Panel} parent Parent panel, used to attach to the DOM
  2742. * @param {Object} [options] See description at Range.setOptions
  2743. */
  2744. function Range(root, parent, options) {
  2745. this.id = util.randomUUID();
  2746. this.start = null; // Number
  2747. this.end = null; // Number
  2748. this.root = root;
  2749. this.parent = parent;
  2750. this.options = options || {};
  2751. // drag listeners for dragging
  2752. this.root.on('dragstart', this._onDragStart.bind(this));
  2753. this.root.on('drag', this._onDrag.bind(this));
  2754. this.root.on('dragend', this._onDragEnd.bind(this));
  2755. // ignore dragging when holding
  2756. this.root.on('hold', this._onHold.bind(this));
  2757. // mouse wheel for zooming
  2758. this.root.on('mousewheel', this._onMouseWheel.bind(this));
  2759. this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  2760. // pinch to zoom
  2761. this.root.on('touch', this._onTouch.bind(this));
  2762. this.root.on('pinch', this._onPinch.bind(this));
  2763. this.setOptions(options);
  2764. }
  2765. // turn Range into an event emitter
  2766. Emitter(Range.prototype);
  2767. /**
  2768. * Set options for the range controller
  2769. * @param {Object} options Available options:
  2770. * {Number} min Minimum value for start
  2771. * {Number} max Maximum value for end
  2772. * {Number} zoomMin Set a minimum value for
  2773. * (end - start).
  2774. * {Number} zoomMax Set a maximum value for
  2775. * (end - start).
  2776. */
  2777. Range.prototype.setOptions = function (options) {
  2778. util.extend(this.options, options);
  2779. // re-apply range with new limitations
  2780. if (this.start !== null && this.end !== null) {
  2781. this.setRange(this.start, this.end);
  2782. }
  2783. };
  2784. /**
  2785. * Test whether direction has a valid value
  2786. * @param {String} direction 'horizontal' or 'vertical'
  2787. */
  2788. function validateDirection (direction) {
  2789. if (direction != 'horizontal' && direction != 'vertical') {
  2790. throw new TypeError('Unknown direction "' + direction + '". ' +
  2791. 'Choose "horizontal" or "vertical".');
  2792. }
  2793. }
  2794. /**
  2795. * Set a new start and end range
  2796. * @param {Number} [start]
  2797. * @param {Number} [end]
  2798. */
  2799. Range.prototype.setRange = function(start, end) {
  2800. var changed = this._applyRange(start, end);
  2801. if (changed) {
  2802. var params = {
  2803. start: new Date(this.start),
  2804. end: new Date(this.end)
  2805. };
  2806. this.emit('rangechange', params);
  2807. this.emit('rangechanged', params);
  2808. }
  2809. };
  2810. /**
  2811. * Set a new start and end range. This method is the same as setRange, but
  2812. * does not trigger a range change and range changed event, and it returns
  2813. * true when the range is changed
  2814. * @param {Number} [start]
  2815. * @param {Number} [end]
  2816. * @return {Boolean} changed
  2817. * @private
  2818. */
  2819. Range.prototype._applyRange = function(start, end) {
  2820. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  2821. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  2822. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2823. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2824. diff;
  2825. // check for valid number
  2826. if (isNaN(newStart) || newStart === null) {
  2827. throw new Error('Invalid start "' + start + '"');
  2828. }
  2829. if (isNaN(newEnd) || newEnd === null) {
  2830. throw new Error('Invalid end "' + end + '"');
  2831. }
  2832. // prevent start < end
  2833. if (newEnd < newStart) {
  2834. newEnd = newStart;
  2835. }
  2836. // prevent start < min
  2837. if (min !== null) {
  2838. if (newStart < min) {
  2839. diff = (min - newStart);
  2840. newStart += diff;
  2841. newEnd += diff;
  2842. // prevent end > max
  2843. if (max != null) {
  2844. if (newEnd > max) {
  2845. newEnd = max;
  2846. }
  2847. }
  2848. }
  2849. }
  2850. // prevent end > max
  2851. if (max !== null) {
  2852. if (newEnd > max) {
  2853. diff = (newEnd - max);
  2854. newStart -= diff;
  2855. newEnd -= diff;
  2856. // prevent start < min
  2857. if (min != null) {
  2858. if (newStart < min) {
  2859. newStart = min;
  2860. }
  2861. }
  2862. }
  2863. }
  2864. // prevent (end-start) < zoomMin
  2865. if (this.options.zoomMin !== null) {
  2866. var zoomMin = parseFloat(this.options.zoomMin);
  2867. if (zoomMin < 0) {
  2868. zoomMin = 0;
  2869. }
  2870. if ((newEnd - newStart) < zoomMin) {
  2871. if ((this.end - this.start) === zoomMin) {
  2872. // ignore this action, we are already zoomed to the minimum
  2873. newStart = this.start;
  2874. newEnd = this.end;
  2875. }
  2876. else {
  2877. // zoom to the minimum
  2878. diff = (zoomMin - (newEnd - newStart));
  2879. newStart -= diff / 2;
  2880. newEnd += diff / 2;
  2881. }
  2882. }
  2883. }
  2884. // prevent (end-start) > zoomMax
  2885. if (this.options.zoomMax !== null) {
  2886. var zoomMax = parseFloat(this.options.zoomMax);
  2887. if (zoomMax < 0) {
  2888. zoomMax = 0;
  2889. }
  2890. if ((newEnd - newStart) > zoomMax) {
  2891. if ((this.end - this.start) === zoomMax) {
  2892. // ignore this action, we are already zoomed to the maximum
  2893. newStart = this.start;
  2894. newEnd = this.end;
  2895. }
  2896. else {
  2897. // zoom to the maximum
  2898. diff = ((newEnd - newStart) - zoomMax);
  2899. newStart += diff / 2;
  2900. newEnd -= diff / 2;
  2901. }
  2902. }
  2903. }
  2904. var changed = (this.start != newStart || this.end != newEnd);
  2905. this.start = newStart;
  2906. this.end = newEnd;
  2907. return changed;
  2908. };
  2909. /**
  2910. * Retrieve the current range.
  2911. * @return {Object} An object with start and end properties
  2912. */
  2913. Range.prototype.getRange = function() {
  2914. return {
  2915. start: this.start,
  2916. end: this.end
  2917. };
  2918. };
  2919. /**
  2920. * Calculate the conversion offset and scale for current range, based on
  2921. * the provided width
  2922. * @param {Number} width
  2923. * @returns {{offset: number, scale: number}} conversion
  2924. */
  2925. Range.prototype.conversion = function (width) {
  2926. return Range.conversion(this.start, this.end, width);
  2927. };
  2928. /**
  2929. * Static method to calculate the conversion offset and scale for a range,
  2930. * based on the provided start, end, and width
  2931. * @param {Number} start
  2932. * @param {Number} end
  2933. * @param {Number} width
  2934. * @returns {{offset: number, scale: number}} conversion
  2935. */
  2936. Range.conversion = function (start, end, width) {
  2937. if (width != 0 && (end - start != 0)) {
  2938. return {
  2939. offset: start,
  2940. scale: width / (end - start)
  2941. }
  2942. }
  2943. else {
  2944. return {
  2945. offset: 0,
  2946. scale: 1
  2947. };
  2948. }
  2949. };
  2950. // global (private) object to store drag params
  2951. var touchParams = {};
  2952. /**
  2953. * Start dragging horizontally or vertically
  2954. * @param {Event} event
  2955. * @private
  2956. */
  2957. Range.prototype._onDragStart = function(event) {
  2958. // refuse to drag when we where pinching to prevent the timeline make a jump
  2959. // when releasing the fingers in opposite order from the touch screen
  2960. if (touchParams.ignore) return;
  2961. // TODO: reckon with option movable
  2962. touchParams.start = this.start;
  2963. touchParams.end = this.end;
  2964. var frame = this.parent.frame;
  2965. if (frame) {
  2966. frame.style.cursor = 'move';
  2967. }
  2968. };
  2969. /**
  2970. * Perform dragging operating.
  2971. * @param {Event} event
  2972. * @private
  2973. */
  2974. Range.prototype._onDrag = function (event) {
  2975. var direction = this.options.direction;
  2976. validateDirection(direction);
  2977. // TODO: reckon with option movable
  2978. // refuse to drag when we where pinching to prevent the timeline make a jump
  2979. // when releasing the fingers in opposite order from the touch screen
  2980. if (touchParams.ignore) return;
  2981. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  2982. interval = (touchParams.end - touchParams.start),
  2983. width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
  2984. diffRange = -delta / width * interval;
  2985. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  2986. this.emit('rangechange', {
  2987. start: new Date(this.start),
  2988. end: new Date(this.end)
  2989. });
  2990. };
  2991. /**
  2992. * Stop dragging operating.
  2993. * @param {event} event
  2994. * @private
  2995. */
  2996. Range.prototype._onDragEnd = function (event) {
  2997. // refuse to drag when we where pinching to prevent the timeline make a jump
  2998. // when releasing the fingers in opposite order from the touch screen
  2999. if (touchParams.ignore) return;
  3000. // TODO: reckon with option movable
  3001. if (this.parent.frame) {
  3002. this.parent.frame.style.cursor = 'auto';
  3003. }
  3004. // fire a rangechanged event
  3005. this.emit('rangechanged', {
  3006. start: new Date(this.start),
  3007. end: new Date(this.end)
  3008. });
  3009. };
  3010. /**
  3011. * Event handler for mouse wheel event, used to zoom
  3012. * Code from http://adomas.org/javascript-mouse-wheel/
  3013. * @param {Event} event
  3014. * @private
  3015. */
  3016. Range.prototype._onMouseWheel = function(event) {
  3017. // TODO: reckon with option zoomable
  3018. // retrieve delta
  3019. var delta = 0;
  3020. if (event.wheelDelta) { /* IE/Opera. */
  3021. delta = event.wheelDelta / 120;
  3022. } else if (event.detail) { /* Mozilla case. */
  3023. // In Mozilla, sign of delta is different than in IE.
  3024. // Also, delta is multiple of 3.
  3025. delta = -event.detail / 3;
  3026. }
  3027. // If delta is nonzero, handle it.
  3028. // Basically, delta is now positive if wheel was scrolled up,
  3029. // and negative, if wheel was scrolled down.
  3030. if (delta) {
  3031. // perform the zoom action. Delta is normally 1 or -1
  3032. // adjust a negative delta such that zooming in with delta 0.1
  3033. // equals zooming out with a delta -0.1
  3034. var scale;
  3035. if (delta < 0) {
  3036. scale = 1 - (delta / 5);
  3037. }
  3038. else {
  3039. scale = 1 / (1 + (delta / 5)) ;
  3040. }
  3041. // calculate center, the date to zoom around
  3042. var gesture = util.fakeGesture(this, event),
  3043. pointer = getPointer(gesture.center, this.parent.frame),
  3044. pointerDate = this._pointerToDate(pointer);
  3045. this.zoom(scale, pointerDate);
  3046. }
  3047. // Prevent default actions caused by mouse wheel
  3048. // (else the page and timeline both zoom and scroll)
  3049. event.preventDefault();
  3050. };
  3051. /**
  3052. * Start of a touch gesture
  3053. * @private
  3054. */
  3055. Range.prototype._onTouch = function (event) {
  3056. touchParams.start = this.start;
  3057. touchParams.end = this.end;
  3058. touchParams.ignore = false;
  3059. touchParams.center = null;
  3060. // don't move the range when dragging a selected event
  3061. // TODO: it's not so neat to have to know about the state of the ItemSet
  3062. var item = ItemSet.itemFromTarget(event);
  3063. if (item && item.selected && this.options.editable) {
  3064. touchParams.ignore = true;
  3065. }
  3066. };
  3067. /**
  3068. * On start of a hold gesture
  3069. * @private
  3070. */
  3071. Range.prototype._onHold = function () {
  3072. touchParams.ignore = true;
  3073. };
  3074. /**
  3075. * Handle pinch event
  3076. * @param {Event} event
  3077. * @private
  3078. */
  3079. Range.prototype._onPinch = function (event) {
  3080. var direction = this.options.direction;
  3081. touchParams.ignore = true;
  3082. // TODO: reckon with option zoomable
  3083. if (event.gesture.touches.length > 1) {
  3084. if (!touchParams.center) {
  3085. touchParams.center = getPointer(event.gesture.center, this.parent.frame);
  3086. }
  3087. var scale = 1 / event.gesture.scale,
  3088. initDate = this._pointerToDate(touchParams.center),
  3089. center = getPointer(event.gesture.center, this.parent.frame),
  3090. date = this._pointerToDate(this.parent, center),
  3091. delta = date - initDate; // TODO: utilize delta
  3092. // calculate new start and end
  3093. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3094. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3095. // apply new range
  3096. this.setRange(newStart, newEnd);
  3097. }
  3098. };
  3099. /**
  3100. * Helper function to calculate the center date for zooming
  3101. * @param {{x: Number, y: Number}} pointer
  3102. * @return {number} date
  3103. * @private
  3104. */
  3105. Range.prototype._pointerToDate = function (pointer) {
  3106. var conversion;
  3107. var direction = this.options.direction;
  3108. validateDirection(direction);
  3109. if (direction == 'horizontal') {
  3110. var width = this.parent.width;
  3111. conversion = this.conversion(width);
  3112. return pointer.x / conversion.scale + conversion.offset;
  3113. }
  3114. else {
  3115. var height = this.parent.height;
  3116. conversion = this.conversion(height);
  3117. return pointer.y / conversion.scale + conversion.offset;
  3118. }
  3119. };
  3120. /**
  3121. * Get the pointer location relative to the location of the dom element
  3122. * @param {{pageX: Number, pageY: Number}} touch
  3123. * @param {Element} element HTML DOM element
  3124. * @return {{x: Number, y: Number}} pointer
  3125. * @private
  3126. */
  3127. function getPointer (touch, element) {
  3128. return {
  3129. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3130. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3131. };
  3132. }
  3133. /**
  3134. * Zoom the range the given scale in or out. Start and end date will
  3135. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3136. * date around which to zoom.
  3137. * For example, try scale = 0.9 or 1.1
  3138. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3139. * values below 1 will zoom in.
  3140. * @param {Number} [center] Value representing a date around which will
  3141. * be zoomed.
  3142. */
  3143. Range.prototype.zoom = function(scale, center) {
  3144. // if centerDate is not provided, take it half between start Date and end Date
  3145. if (center == null) {
  3146. center = (this.start + this.end) / 2;
  3147. }
  3148. // calculate new start and end
  3149. var newStart = center + (this.start - center) * scale;
  3150. var newEnd = center + (this.end - center) * scale;
  3151. this.setRange(newStart, newEnd);
  3152. };
  3153. /**
  3154. * Move the range with a given delta to the left or right. Start and end
  3155. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3156. * @param {Number} delta Moving amount. Positive value will move right,
  3157. * negative value will move left
  3158. */
  3159. Range.prototype.move = function(delta) {
  3160. // zoom start Date and end Date relative to the centerDate
  3161. var diff = (this.end - this.start);
  3162. // apply new values
  3163. var newStart = this.start + diff * delta;
  3164. var newEnd = this.end + diff * delta;
  3165. // TODO: reckon with min and max range
  3166. this.start = newStart;
  3167. this.end = newEnd;
  3168. };
  3169. /**
  3170. * Move the range to a new center point
  3171. * @param {Number} moveTo New center point of the range
  3172. */
  3173. Range.prototype.moveTo = function(moveTo) {
  3174. var center = (this.start + this.end) / 2;
  3175. var diff = center - moveTo;
  3176. // calculate new start and end
  3177. var newStart = this.start - diff;
  3178. var newEnd = this.end - diff;
  3179. this.setRange(newStart, newEnd);
  3180. };
  3181. /**
  3182. * Prototype for visual components
  3183. */
  3184. function Component () {
  3185. this.id = null;
  3186. this.parent = null;
  3187. this.childs = null;
  3188. this.options = null;
  3189. this.top = 0;
  3190. this.left = 0;
  3191. this.width = 0;
  3192. this.height = 0;
  3193. }
  3194. // Turn the Component into an event emitter
  3195. Emitter(Component.prototype);
  3196. /**
  3197. * Set parameters for the frame. Parameters will be merged in current parameter
  3198. * set.
  3199. * @param {Object} options Available parameters:
  3200. * {String | function} [className]
  3201. * {String | Number | function} [left]
  3202. * {String | Number | function} [top]
  3203. * {String | Number | function} [width]
  3204. * {String | Number | function} [height]
  3205. */
  3206. Component.prototype.setOptions = function setOptions(options) {
  3207. if (options) {
  3208. util.extend(this.options, options);
  3209. this.repaint();
  3210. }
  3211. };
  3212. /**
  3213. * Get an option value by name
  3214. * The function will first check this.options object, and else will check
  3215. * this.defaultOptions.
  3216. * @param {String} name
  3217. * @return {*} value
  3218. */
  3219. Component.prototype.getOption = function getOption(name) {
  3220. var value;
  3221. if (this.options) {
  3222. value = this.options[name];
  3223. }
  3224. if (value === undefined && this.defaultOptions) {
  3225. value = this.defaultOptions[name];
  3226. }
  3227. return value;
  3228. };
  3229. /**
  3230. * Get the frame element of the component, the outer HTML DOM element.
  3231. * @returns {HTMLElement | null} frame
  3232. */
  3233. Component.prototype.getFrame = function getFrame() {
  3234. // should be implemented by the component
  3235. return null;
  3236. };
  3237. /**
  3238. * Repaint the component
  3239. * @return {boolean} Returns true if the component is resized
  3240. */
  3241. Component.prototype.repaint = function repaint() {
  3242. // should be implemented by the component
  3243. return false;
  3244. };
  3245. /**
  3246. * Test whether the component is resized since the last time _isResized() was
  3247. * called.
  3248. * @return {Boolean} Returns true if the component is resized
  3249. * @private
  3250. */
  3251. Component.prototype._isResized = function _isResized() {
  3252. var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
  3253. this._previousWidth = this.width;
  3254. this._previousHeight = this.height;
  3255. return resized;
  3256. };
  3257. /**
  3258. * A panel can contain components
  3259. * @param {Object} [options] Available parameters:
  3260. * {String | Number | function} [left]
  3261. * {String | Number | function} [top]
  3262. * {String | Number | function} [width]
  3263. * {String | Number | function} [height]
  3264. * {String | function} [className]
  3265. * @constructor Panel
  3266. * @extends Component
  3267. */
  3268. function Panel(options) {
  3269. this.id = util.randomUUID();
  3270. this.parent = null;
  3271. this.childs = [];
  3272. this.options = options || {};
  3273. // create frame
  3274. this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
  3275. }
  3276. Panel.prototype = new Component();
  3277. /**
  3278. * Set options. Will extend the current options.
  3279. * @param {Object} [options] Available parameters:
  3280. * {String | function} [className]
  3281. * {String | Number | function} [left]
  3282. * {String | Number | function} [top]
  3283. * {String | Number | function} [width]
  3284. * {String | Number | function} [height]
  3285. */
  3286. Panel.prototype.setOptions = Component.prototype.setOptions;
  3287. /**
  3288. * Get the outer frame of the panel
  3289. * @returns {HTMLElement} frame
  3290. */
  3291. Panel.prototype.getFrame = function () {
  3292. return this.frame;
  3293. };
  3294. /**
  3295. * Append a child to the panel
  3296. * @param {Component} child
  3297. */
  3298. Panel.prototype.appendChild = function (child) {
  3299. this.childs.push(child);
  3300. child.parent = this;
  3301. // attach to the DOM
  3302. var frame = child.getFrame();
  3303. if (frame) {
  3304. if (frame.parentNode) {
  3305. frame.parentNode.removeChild(frame);
  3306. }
  3307. this.frame.appendChild(frame);
  3308. }
  3309. };
  3310. /**
  3311. * Insert a child to the panel
  3312. * @param {Component} child
  3313. * @param {Component} beforeChild
  3314. */
  3315. Panel.prototype.insertBefore = function (child, beforeChild) {
  3316. var index = this.childs.indexOf(beforeChild);
  3317. if (index != -1) {
  3318. this.childs.splice(index, 0, child);
  3319. child.parent = this;
  3320. // attach to the DOM
  3321. var frame = child.getFrame();
  3322. if (frame) {
  3323. if (frame.parentNode) {
  3324. frame.parentNode.removeChild(frame);
  3325. }
  3326. var beforeFrame = beforeChild.getFrame();
  3327. if (beforeFrame) {
  3328. this.frame.insertBefore(frame, beforeFrame);
  3329. }
  3330. else {
  3331. this.frame.appendChild(frame);
  3332. }
  3333. }
  3334. }
  3335. };
  3336. /**
  3337. * Remove a child from the panel
  3338. * @param {Component} child
  3339. */
  3340. Panel.prototype.removeChild = function (child) {
  3341. var index = this.childs.indexOf(child);
  3342. if (index != -1) {
  3343. this.childs.splice(index, 1);
  3344. child.parent = null;
  3345. // remove from the DOM
  3346. var frame = child.getFrame();
  3347. if (frame && frame.parentNode) {
  3348. this.frame.removeChild(frame);
  3349. }
  3350. }
  3351. };
  3352. /**
  3353. * Test whether the panel contains given child
  3354. * @param {Component} child
  3355. */
  3356. Panel.prototype.hasChild = function (child) {
  3357. var index = this.childs.indexOf(child);
  3358. return (index != -1);
  3359. };
  3360. /**
  3361. * Repaint the component
  3362. * @return {boolean} Returns true if the component was resized since previous repaint
  3363. */
  3364. Panel.prototype.repaint = function () {
  3365. var asString = util.option.asString,
  3366. options = this.options,
  3367. frame = this.getFrame();
  3368. // update className
  3369. frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
  3370. // repaint the child components
  3371. var childsResized = this._repaintChilds();
  3372. // update frame size
  3373. this._updateSize();
  3374. return this._isResized() || childsResized;
  3375. };
  3376. /**
  3377. * Repaint all childs of the panel
  3378. * @return {boolean} Returns true if the component is resized
  3379. * @private
  3380. */
  3381. Panel.prototype._repaintChilds = function () {
  3382. var resized = false;
  3383. for (var i = 0, ii = this.childs.length; i < ii; i++) {
  3384. resized = this.childs[i].repaint() || resized;
  3385. }
  3386. return resized;
  3387. };
  3388. /**
  3389. * Apply the size from options to the panel, and recalculate it's actual size.
  3390. * @private
  3391. */
  3392. Panel.prototype._updateSize = function () {
  3393. // apply size
  3394. this.frame.style.top = util.option.asSize(this.options.top);
  3395. this.frame.style.bottom = util.option.asSize(this.options.bottom);
  3396. this.frame.style.left = util.option.asSize(this.options.left);
  3397. this.frame.style.right = util.option.asSize(this.options.right);
  3398. this.frame.style.width = util.option.asSize(this.options.width, '100%');
  3399. this.frame.style.height = util.option.asSize(this.options.height, '');
  3400. // get actual size
  3401. this.top = this.frame.offsetTop;
  3402. this.left = this.frame.offsetLeft;
  3403. this.width = this.frame.offsetWidth;
  3404. this.height = this.frame.offsetHeight;
  3405. };
  3406. /**
  3407. * A root panel can hold components. The root panel must be initialized with
  3408. * a DOM element as container.
  3409. * @param {HTMLElement} container
  3410. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3411. * @constructor RootPanel
  3412. * @extends Panel
  3413. */
  3414. function RootPanel(container, options) {
  3415. this.id = util.randomUUID();
  3416. this.container = container;
  3417. this.options = options || {};
  3418. this.defaultOptions = {
  3419. autoResize: true
  3420. };
  3421. // create the HTML DOM
  3422. this._create();
  3423. // attach the root panel to the provided container
  3424. if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
  3425. this.container.appendChild(this.getFrame());
  3426. this._initWatch();
  3427. }
  3428. RootPanel.prototype = new Panel();
  3429. /**
  3430. * Create the HTML DOM for the root panel
  3431. */
  3432. RootPanel.prototype._create = function _create() {
  3433. // create frame
  3434. this.frame = document.createElement('div');
  3435. // create event listeners for all interesting events, these events will be
  3436. // emitted via emitter
  3437. this.hammer = Hammer(this.frame, {
  3438. prevent_default: true
  3439. });
  3440. this.listeners = {};
  3441. var me = this;
  3442. var events = [
  3443. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3444. 'dragstart', 'drag', 'dragend',
  3445. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3446. ];
  3447. events.forEach(function (event) {
  3448. var listener = function () {
  3449. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3450. me.emit.apply(me, args);
  3451. };
  3452. me.hammer.on(event, listener);
  3453. me.listeners[event] = listener;
  3454. });
  3455. };
  3456. /**
  3457. * Set options. Will extend the current options.
  3458. * @param {Object} [options] Available parameters:
  3459. * {String | function} [className]
  3460. * {String | Number | function} [left]
  3461. * {String | Number | function} [top]
  3462. * {String | Number | function} [width]
  3463. * {String | Number | function} [height]
  3464. * {Boolean | function} [autoResize]
  3465. */
  3466. RootPanel.prototype.setOptions = function setOptions(options) {
  3467. if (options) {
  3468. util.extend(this.options, options);
  3469. this.repaint();
  3470. this._initWatch();
  3471. }
  3472. };
  3473. /**
  3474. * Get the frame of the root panel
  3475. */
  3476. RootPanel.prototype.getFrame = function getFrame() {
  3477. return this.frame;
  3478. };
  3479. /**
  3480. * Repaint the root panel
  3481. */
  3482. RootPanel.prototype.repaint = function repaint() {
  3483. // update class name
  3484. var options = this.options;
  3485. var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
  3486. if (options.className) className += ' ' + util.option.asString(className);
  3487. this.frame.className = className;
  3488. // repaint the child components
  3489. var childsResized = this._repaintChilds();
  3490. // update frame size
  3491. this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
  3492. this._updateSize();
  3493. // if the root panel or any of its childs is resized, repaint again,
  3494. // as other components may need to be resized accordingly
  3495. var resized = this._isResized() || childsResized;
  3496. if (resized) {
  3497. setTimeout(this.repaint.bind(this), 0);
  3498. }
  3499. };
  3500. /**
  3501. * Initialize watching when option autoResize is true
  3502. * @private
  3503. */
  3504. RootPanel.prototype._initWatch = function _initWatch() {
  3505. var autoResize = this.getOption('autoResize');
  3506. if (autoResize) {
  3507. this._watch();
  3508. }
  3509. else {
  3510. this._unwatch();
  3511. }
  3512. };
  3513. /**
  3514. * Watch for changes in the size of the frame. On resize, the Panel will
  3515. * automatically redraw itself.
  3516. * @private
  3517. */
  3518. RootPanel.prototype._watch = function _watch() {
  3519. var me = this;
  3520. this._unwatch();
  3521. var checkSize = function checkSize() {
  3522. var autoResize = me.getOption('autoResize');
  3523. if (!autoResize) {
  3524. // stop watching when the option autoResize is changed to false
  3525. me._unwatch();
  3526. return;
  3527. }
  3528. if (me.frame) {
  3529. // check whether the frame is resized
  3530. if ((me.frame.clientWidth != me.lastWidth) ||
  3531. (me.frame.clientHeight != me.lastHeight)) {
  3532. me.lastWidth = me.frame.clientWidth;
  3533. me.lastHeight = me.frame.clientHeight;
  3534. me.repaint();
  3535. // TODO: emit a resize event instead?
  3536. }
  3537. }
  3538. };
  3539. // TODO: automatically cleanup the event listener when the frame is deleted
  3540. util.addEventListener(window, 'resize', checkSize);
  3541. this.watchTimer = setInterval(checkSize, 1000);
  3542. };
  3543. /**
  3544. * Stop watching for a resize of the frame.
  3545. * @private
  3546. */
  3547. RootPanel.prototype._unwatch = function _unwatch() {
  3548. if (this.watchTimer) {
  3549. clearInterval(this.watchTimer);
  3550. this.watchTimer = undefined;
  3551. }
  3552. // TODO: remove event listener on window.resize
  3553. };
  3554. /**
  3555. * A horizontal time axis
  3556. * @param {Object} [options] See TimeAxis.setOptions for the available
  3557. * options.
  3558. * @constructor TimeAxis
  3559. * @extends Component
  3560. */
  3561. function TimeAxis (options) {
  3562. this.id = util.randomUUID();
  3563. this.dom = {
  3564. majorLines: [],
  3565. majorTexts: [],
  3566. minorLines: [],
  3567. minorTexts: [],
  3568. redundant: {
  3569. majorLines: [],
  3570. majorTexts: [],
  3571. minorLines: [],
  3572. minorTexts: []
  3573. }
  3574. };
  3575. this.props = {
  3576. range: {
  3577. start: 0,
  3578. end: 0,
  3579. minimumStep: 0
  3580. },
  3581. lineTop: 0
  3582. };
  3583. this.options = options || {};
  3584. this.defaultOptions = {
  3585. orientation: 'bottom', // supported: 'top', 'bottom'
  3586. // TODO: implement timeaxis orientations 'left' and 'right'
  3587. showMinorLabels: true,
  3588. showMajorLabels: true
  3589. };
  3590. this.range = null;
  3591. // create the HTML DOM
  3592. this._create();
  3593. }
  3594. TimeAxis.prototype = new Component();
  3595. // TODO: comment options
  3596. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3597. /**
  3598. * Create the HTML DOM for the TimeAxis
  3599. */
  3600. TimeAxis.prototype._create = function _create() {
  3601. this.frame = document.createElement('div');
  3602. };
  3603. /**
  3604. * Set a range (start and end)
  3605. * @param {Range | Object} range A Range or an object containing start and end.
  3606. */
  3607. TimeAxis.prototype.setRange = function (range) {
  3608. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3609. throw new TypeError('Range must be an instance of Range, ' +
  3610. 'or an object containing start and end.');
  3611. }
  3612. this.range = range;
  3613. };
  3614. /**
  3615. * Get the outer frame of the time axis
  3616. * @return {HTMLElement} frame
  3617. */
  3618. TimeAxis.prototype.getFrame = function getFrame() {
  3619. return this.frame;
  3620. };
  3621. /**
  3622. * Repaint the component
  3623. * @return {boolean} Returns true if the component is resized
  3624. */
  3625. TimeAxis.prototype.repaint = function () {
  3626. var asSize = util.option.asSize,
  3627. options = this.options,
  3628. props = this.props,
  3629. frame = this.frame;
  3630. // update classname
  3631. frame.className = 'timeaxis'; // TODO: add className from options if defined
  3632. var parent = frame.parentNode;
  3633. if (parent) {
  3634. // calculate character width and height
  3635. this._calculateCharSize();
  3636. // TODO: recalculate sizes only needed when parent is resized or options is changed
  3637. var orientation = this.getOption('orientation'),
  3638. showMinorLabels = this.getOption('showMinorLabels'),
  3639. showMajorLabels = this.getOption('showMajorLabels');
  3640. // determine the width and height of the elemens for the axis
  3641. var parentHeight = this.parent.height;
  3642. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3643. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3644. this.height = props.minorLabelHeight + props.majorLabelHeight;
  3645. this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
  3646. props.minorLineHeight = parentHeight + props.minorLabelHeight;
  3647. props.minorLineWidth = 1; // TODO: really calculate width
  3648. props.majorLineHeight = parentHeight + this.height;
  3649. props.majorLineWidth = 1; // TODO: really calculate width
  3650. // take frame offline while updating (is almost twice as fast)
  3651. var beforeChild = frame.nextSibling;
  3652. parent.removeChild(frame);
  3653. // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
  3654. if (orientation == 'top') {
  3655. frame.style.top = '0';
  3656. frame.style.left = '0';
  3657. frame.style.bottom = '';
  3658. frame.style.width = asSize(options.width, '100%');
  3659. frame.style.height = this.height + 'px';
  3660. }
  3661. else { // bottom
  3662. frame.style.top = '';
  3663. frame.style.bottom = '0';
  3664. frame.style.left = '0';
  3665. frame.style.width = asSize(options.width, '100%');
  3666. frame.style.height = this.height + 'px';
  3667. }
  3668. this._repaintLabels();
  3669. this._repaintLine();
  3670. // put frame online again
  3671. if (beforeChild) {
  3672. parent.insertBefore(frame, beforeChild);
  3673. }
  3674. else {
  3675. parent.appendChild(frame)
  3676. }
  3677. }
  3678. return this._isResized();
  3679. };
  3680. /**
  3681. * Repaint major and minor text labels and vertical grid lines
  3682. * @private
  3683. */
  3684. TimeAxis.prototype._repaintLabels = function () {
  3685. var orientation = this.getOption('orientation');
  3686. // calculate range and step
  3687. var start = util.convert(this.range.start, 'Number'),
  3688. end = util.convert(this.range.end, 'Number'),
  3689. minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 5).valueOf()
  3690. -this.options.toTime(0).valueOf();
  3691. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  3692. this.step = step;
  3693. // Move all DOM elements to a "redundant" list, where they
  3694. // can be picked for re-use, and clear the lists with lines and texts.
  3695. // At the end of the function _repaintLabels, left over elements will be cleaned up
  3696. var dom = this.dom;
  3697. dom.redundant.majorLines = dom.majorLines;
  3698. dom.redundant.majorTexts = dom.majorTexts;
  3699. dom.redundant.minorLines = dom.minorLines;
  3700. dom.redundant.minorTexts = dom.minorTexts;
  3701. dom.majorLines = [];
  3702. dom.majorTexts = [];
  3703. dom.minorLines = [];
  3704. dom.minorTexts = [];
  3705. step.first();
  3706. var xFirstMajorLabel = undefined;
  3707. var max = 0;
  3708. while (step.hasNext() && max < 1000) {
  3709. max++;
  3710. var cur = step.getCurrent(),
  3711. x = this.options.toScreen(cur),
  3712. isMajor = step.isMajor();
  3713. // TODO: lines must have a width, such that we can create css backgrounds
  3714. if (this.getOption('showMinorLabels')) {
  3715. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  3716. }
  3717. if (isMajor && this.getOption('showMajorLabels')) {
  3718. if (x > 0) {
  3719. if (xFirstMajorLabel == undefined) {
  3720. xFirstMajorLabel = x;
  3721. }
  3722. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  3723. }
  3724. this._repaintMajorLine(x, orientation);
  3725. }
  3726. else {
  3727. this._repaintMinorLine(x, orientation);
  3728. }
  3729. step.next();
  3730. }
  3731. // create a major label on the left when needed
  3732. if (this.getOption('showMajorLabels')) {
  3733. var leftTime = this.options.toTime(0),
  3734. leftText = step.getLabelMajor(leftTime),
  3735. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  3736. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3737. this._repaintMajorText(0, leftText, orientation);
  3738. }
  3739. }
  3740. // Cleanup leftover DOM elements from the redundant list
  3741. util.forEach(this.dom.redundant, function (arr) {
  3742. while (arr.length) {
  3743. var elem = arr.pop();
  3744. if (elem && elem.parentNode) {
  3745. elem.parentNode.removeChild(elem);
  3746. }
  3747. }
  3748. });
  3749. };
  3750. /**
  3751. * Create a minor label for the axis at position x
  3752. * @param {Number} x
  3753. * @param {String} text
  3754. * @param {String} orientation "top" or "bottom" (default)
  3755. * @private
  3756. */
  3757. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  3758. // reuse redundant label
  3759. var label = this.dom.redundant.minorTexts.shift();
  3760. if (!label) {
  3761. // create new label
  3762. var content = document.createTextNode('');
  3763. label = document.createElement('div');
  3764. label.appendChild(content);
  3765. label.className = 'text minor';
  3766. this.frame.appendChild(label);
  3767. }
  3768. this.dom.minorTexts.push(label);
  3769. label.childNodes[0].nodeValue = text;
  3770. if (orientation == 'top') {
  3771. label.style.top = this.props.majorLabelHeight + 'px';
  3772. label.style.bottom = '';
  3773. }
  3774. else {
  3775. label.style.top = '';
  3776. label.style.bottom = this.props.majorLabelHeight + 'px';
  3777. }
  3778. label.style.left = x + 'px';
  3779. //label.title = title; // TODO: this is a heavy operation
  3780. };
  3781. /**
  3782. * Create a Major label for the axis at position x
  3783. * @param {Number} x
  3784. * @param {String} text
  3785. * @param {String} orientation "top" or "bottom" (default)
  3786. * @private
  3787. */
  3788. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  3789. // reuse redundant label
  3790. var label = this.dom.redundant.majorTexts.shift();
  3791. if (!label) {
  3792. // create label
  3793. var content = document.createTextNode(text);
  3794. label = document.createElement('div');
  3795. label.className = 'text major';
  3796. label.appendChild(content);
  3797. this.frame.appendChild(label);
  3798. }
  3799. this.dom.majorTexts.push(label);
  3800. label.childNodes[0].nodeValue = text;
  3801. //label.title = title; // TODO: this is a heavy operation
  3802. if (orientation == 'top') {
  3803. label.style.top = '0px';
  3804. label.style.bottom = '';
  3805. }
  3806. else {
  3807. label.style.top = '';
  3808. label.style.bottom = '0px';
  3809. }
  3810. label.style.left = x + 'px';
  3811. };
  3812. /**
  3813. * Create a minor line for the axis at position x
  3814. * @param {Number} x
  3815. * @param {String} orientation "top" or "bottom" (default)
  3816. * @private
  3817. */
  3818. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  3819. // reuse redundant line
  3820. var line = this.dom.redundant.minorLines.shift();
  3821. if (!line) {
  3822. // create vertical line
  3823. line = document.createElement('div');
  3824. line.className = 'grid vertical minor';
  3825. this.frame.appendChild(line);
  3826. }
  3827. this.dom.minorLines.push(line);
  3828. var props = this.props;
  3829. if (orientation == 'top') {
  3830. line.style.top = this.props.majorLabelHeight + 'px';
  3831. line.style.bottom = '';
  3832. }
  3833. else {
  3834. line.style.top = '';
  3835. line.style.bottom = this.props.majorLabelHeight + 'px';
  3836. }
  3837. line.style.height = props.minorLineHeight + 'px';
  3838. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3839. };
  3840. /**
  3841. * Create a Major line for the axis at position x
  3842. * @param {Number} x
  3843. * @param {String} orientation "top" or "bottom" (default)
  3844. * @private
  3845. */
  3846. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  3847. // reuse redundant line
  3848. var line = this.dom.redundant.majorLines.shift();
  3849. if (!line) {
  3850. // create vertical line
  3851. line = document.createElement('DIV');
  3852. line.className = 'grid vertical major';
  3853. this.frame.appendChild(line);
  3854. }
  3855. this.dom.majorLines.push(line);
  3856. var props = this.props;
  3857. if (orientation == 'top') {
  3858. line.style.top = '0px';
  3859. line.style.bottom = '';
  3860. }
  3861. else {
  3862. line.style.top = '';
  3863. line.style.bottom = '0px';
  3864. }
  3865. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3866. line.style.height = props.majorLineHeight + 'px';
  3867. };
  3868. /**
  3869. * Repaint the horizontal line for the axis
  3870. * @private
  3871. */
  3872. TimeAxis.prototype._repaintLine = function() {
  3873. var line = this.dom.line,
  3874. frame = this.frame,
  3875. orientation = this.getOption('orientation');
  3876. // line before all axis elements
  3877. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  3878. if (line) {
  3879. // put this line at the end of all childs
  3880. frame.removeChild(line);
  3881. frame.appendChild(line);
  3882. }
  3883. else {
  3884. // create the axis line
  3885. line = document.createElement('div');
  3886. line.className = 'grid horizontal major';
  3887. frame.appendChild(line);
  3888. this.dom.line = line;
  3889. }
  3890. if (orientation == 'top') {
  3891. line.style.top = this.height + 'px';
  3892. line.style.bottom = '';
  3893. }
  3894. else {
  3895. line.style.top = '';
  3896. line.style.bottom = this.height + 'px';
  3897. }
  3898. }
  3899. else {
  3900. if (line && line.parentNode) {
  3901. line.parentNode.removeChild(line);
  3902. delete this.dom.line;
  3903. }
  3904. }
  3905. };
  3906. /**
  3907. * Determine the size of text on the axis (both major and minor axis).
  3908. * The size is calculated only once and then cached in this.props.
  3909. * @private
  3910. */
  3911. TimeAxis.prototype._calculateCharSize = function () {
  3912. // determine the char width and height on the minor axis
  3913. if (!('minorCharHeight' in this.props)) {
  3914. var textMinor = document.createTextNode('0');
  3915. var measureCharMinor = document.createElement('DIV');
  3916. measureCharMinor.className = 'text minor measure';
  3917. measureCharMinor.appendChild(textMinor);
  3918. this.frame.appendChild(measureCharMinor);
  3919. this.props.minorCharHeight = measureCharMinor.clientHeight;
  3920. this.props.minorCharWidth = measureCharMinor.clientWidth;
  3921. this.frame.removeChild(measureCharMinor);
  3922. }
  3923. if (!('majorCharHeight' in this.props)) {
  3924. var textMajor = document.createTextNode('0');
  3925. var measureCharMajor = document.createElement('DIV');
  3926. measureCharMajor.className = 'text major measure';
  3927. measureCharMajor.appendChild(textMajor);
  3928. this.frame.appendChild(measureCharMajor);
  3929. this.props.majorCharHeight = measureCharMajor.clientHeight;
  3930. this.props.majorCharWidth = measureCharMajor.clientWidth;
  3931. this.frame.removeChild(measureCharMajor);
  3932. }
  3933. };
  3934. /**
  3935. * Snap a date to a rounded value.
  3936. * The snap intervals are dependent on the current scale and step.
  3937. * @param {Date} date the date to be snapped.
  3938. * @return {Date} snappedDate
  3939. */
  3940. TimeAxis.prototype.snap = function snap (date) {
  3941. return this.step.snap(date);
  3942. };
  3943. /**
  3944. * A current time bar
  3945. * @param {Range} range
  3946. * @param {Object} [options] Available parameters:
  3947. * {Boolean} [showCurrentTime]
  3948. * @constructor CurrentTime
  3949. * @extends Component
  3950. */
  3951. function CurrentTime (range, options) {
  3952. this.id = util.randomUUID();
  3953. this.range = range;
  3954. this.options = options || {};
  3955. this.defaultOptions = {
  3956. showCurrentTime: false
  3957. };
  3958. this._create();
  3959. }
  3960. CurrentTime.prototype = new Component();
  3961. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  3962. /**
  3963. * Create the HTML DOM for the current time bar
  3964. * @private
  3965. */
  3966. CurrentTime.prototype._create = function _create () {
  3967. var bar = document.createElement('div');
  3968. bar.className = 'currenttime';
  3969. bar.style.position = 'absolute';
  3970. bar.style.top = '0px';
  3971. bar.style.height = '100%';
  3972. this.bar = bar;
  3973. };
  3974. /**
  3975. * Get the frame element of the current time bar
  3976. * @returns {HTMLElement} frame
  3977. */
  3978. CurrentTime.prototype.getFrame = function getFrame() {
  3979. return this.bar;
  3980. };
  3981. /**
  3982. * Repaint the component
  3983. * @return {boolean} Returns true if the component is resized
  3984. */
  3985. CurrentTime.prototype.repaint = function repaint() {
  3986. var parent = this.parent;
  3987. var now = new Date();
  3988. var x = this.options.toScreen(now);
  3989. this.bar.style.left = x + 'px';
  3990. this.bar.title = 'Current time: ' + now;
  3991. return false;
  3992. };
  3993. /**
  3994. * Start auto refreshing the current time bar
  3995. */
  3996. CurrentTime.prototype.start = function start() {
  3997. var me = this;
  3998. function update () {
  3999. me.stop();
  4000. // determine interval to refresh
  4001. var scale = me.range.conversion(me.parent.width).scale;
  4002. var interval = 1 / scale / 10;
  4003. if (interval < 30) interval = 30;
  4004. if (interval > 1000) interval = 1000;
  4005. me.repaint();
  4006. // start a timer to adjust for the new time
  4007. me.currentTimeTimer = setTimeout(update, interval);
  4008. }
  4009. update();
  4010. };
  4011. /**
  4012. * Stop auto refreshing the current time bar
  4013. */
  4014. CurrentTime.prototype.stop = function stop() {
  4015. if (this.currentTimeTimer !== undefined) {
  4016. clearTimeout(this.currentTimeTimer);
  4017. delete this.currentTimeTimer;
  4018. }
  4019. };
  4020. /**
  4021. * A custom time bar
  4022. * @param {Object} [options] Available parameters:
  4023. * {Boolean} [showCustomTime]
  4024. * @constructor CustomTime
  4025. * @extends Component
  4026. */
  4027. function CustomTime (options) {
  4028. this.id = util.randomUUID();
  4029. this.options = options || {};
  4030. this.defaultOptions = {
  4031. showCustomTime: false
  4032. };
  4033. this.customTime = new Date();
  4034. this.eventParams = {}; // stores state parameters while dragging the bar
  4035. // create the DOM
  4036. this._create();
  4037. }
  4038. CustomTime.prototype = new Component();
  4039. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4040. /**
  4041. * Create the DOM for the custom time
  4042. * @private
  4043. */
  4044. CustomTime.prototype._create = function _create () {
  4045. var bar = document.createElement('div');
  4046. bar.className = 'customtime';
  4047. bar.style.position = 'absolute';
  4048. bar.style.top = '0px';
  4049. bar.style.height = '100%';
  4050. this.bar = bar;
  4051. var drag = document.createElement('div');
  4052. drag.style.position = 'relative';
  4053. drag.style.top = '0px';
  4054. drag.style.left = '-10px';
  4055. drag.style.height = '100%';
  4056. drag.style.width = '20px';
  4057. bar.appendChild(drag);
  4058. // attach event listeners
  4059. this.hammer = Hammer(bar, {
  4060. prevent_default: true
  4061. });
  4062. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4063. this.hammer.on('drag', this._onDrag.bind(this));
  4064. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4065. };
  4066. /**
  4067. * Get the frame element of the custom time bar
  4068. * @returns {HTMLElement} frame
  4069. */
  4070. CustomTime.prototype.getFrame = function getFrame() {
  4071. return this.bar;
  4072. };
  4073. /**
  4074. * Repaint the component
  4075. * @return {boolean} Returns true if the component is resized
  4076. */
  4077. CustomTime.prototype.repaint = function () {
  4078. var x = this.options.toScreen(this.customTime);
  4079. this.bar.style.left = x + 'px';
  4080. this.bar.title = 'Time: ' + this.customTime;
  4081. return false;
  4082. };
  4083. /**
  4084. * Set custom time.
  4085. * @param {Date} time
  4086. */
  4087. CustomTime.prototype.setCustomTime = function(time) {
  4088. this.customTime = new Date(time.valueOf());
  4089. this.repaint();
  4090. };
  4091. /**
  4092. * Retrieve the current custom time.
  4093. * @return {Date} customTime
  4094. */
  4095. CustomTime.prototype.getCustomTime = function() {
  4096. return new Date(this.customTime.valueOf());
  4097. };
  4098. /**
  4099. * Start moving horizontally
  4100. * @param {Event} event
  4101. * @private
  4102. */
  4103. CustomTime.prototype._onDragStart = function(event) {
  4104. this.eventParams.dragging = true;
  4105. this.eventParams.customTime = this.customTime;
  4106. event.stopPropagation();
  4107. event.preventDefault();
  4108. };
  4109. /**
  4110. * Perform moving operating.
  4111. * @param {Event} event
  4112. * @private
  4113. */
  4114. CustomTime.prototype._onDrag = function (event) {
  4115. if (!this.eventParams.dragging) return;
  4116. var deltaX = event.gesture.deltaX,
  4117. x = this.options.toScreen(this.eventParams.customTime) + deltaX,
  4118. time = this.options.toTime(x);
  4119. this.setCustomTime(time);
  4120. // fire a timechange event
  4121. this.emit('timechange', {
  4122. time: new Date(this.customTime.valueOf())
  4123. });
  4124. event.stopPropagation();
  4125. event.preventDefault();
  4126. };
  4127. /**
  4128. * Stop moving operating.
  4129. * @param {event} event
  4130. * @private
  4131. */
  4132. CustomTime.prototype._onDragEnd = function (event) {
  4133. if (!this.eventParams.dragging) return;
  4134. // fire a timechanged event
  4135. this.emit('timechanged', {
  4136. time: new Date(this.customTime.valueOf())
  4137. });
  4138. event.stopPropagation();
  4139. event.preventDefault();
  4140. };
  4141. /**
  4142. * An ItemSet holds a set of items and ranges which can be displayed in a
  4143. * range. The width is determined by the parent of the ItemSet, and the height
  4144. * is determined by the size of the items.
  4145. * @param {Panel} backgroundPanel Panel which can be used to display the
  4146. * vertical lines of box items.
  4147. * @param {Panel} axisPanel Panel on the axis where the dots of box-items
  4148. * can be displayed.
  4149. * @param {Object} [options] See ItemSet.setOptions for the available options.
  4150. * @constructor ItemSet
  4151. * @extends Panel
  4152. */
  4153. function ItemSet(backgroundPanel, axisPanel, options) {
  4154. this.id = util.randomUUID();
  4155. // one options object is shared by this itemset and all its items
  4156. this.options = options || {};
  4157. this.backgroundPanel = backgroundPanel;
  4158. this.axisPanel = axisPanel;
  4159. this.itemOptions = Object.create(this.options);
  4160. this.dom = {};
  4161. this.hammer = null;
  4162. var me = this;
  4163. this.itemsData = null; // DataSet
  4164. this.range = null; // Range or Object {start: number, end: number}
  4165. // data change listeners
  4166. this.listeners = {
  4167. 'add': function (event, params, senderId) {
  4168. if (senderId != me.id) me._onAdd(params.items);
  4169. },
  4170. 'update': function (event, params, senderId) {
  4171. if (senderId != me.id) me._onUpdate(params.items);
  4172. },
  4173. 'remove': function (event, params, senderId) {
  4174. if (senderId != me.id) me._onRemove(params.items);
  4175. }
  4176. };
  4177. this.items = {}; // object with an Item for every data item
  4178. this.orderedItems = {
  4179. byStart: [],
  4180. byEnd: []
  4181. };
  4182. this.visibleItems = []; // visible, ordered items
  4183. this.visibleItemsStart = 0; // start index of visible items in this.orderedItems // TODO: cleanup
  4184. this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup
  4185. this.selection = []; // list with the ids of all selected nodes
  4186. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4187. this.stack = new Stack(Object.create(this.options));
  4188. this.stackDirty = true; // if true, all items will be restacked on next repaint
  4189. this.touchParams = {}; // stores properties while dragging
  4190. // create the HTML DOM
  4191. this._create();
  4192. }
  4193. ItemSet.prototype = new Panel();
  4194. // available item types will be registered here
  4195. ItemSet.types = {
  4196. box: ItemBox,
  4197. range: ItemRange,
  4198. rangeoverflow: ItemRangeOverflow,
  4199. point: ItemPoint
  4200. };
  4201. /**
  4202. * Create the HTML DOM for the ItemSet
  4203. */
  4204. ItemSet.prototype._create = function _create(){
  4205. var frame = document.createElement('div');
  4206. frame['timeline-itemset'] = this;
  4207. this.frame = frame;
  4208. // create background panel
  4209. var background = document.createElement('div');
  4210. background.className = 'background';
  4211. this.backgroundPanel.frame.appendChild(background);
  4212. this.dom.background = background;
  4213. // create foreground panel
  4214. var foreground = document.createElement('div');
  4215. foreground.className = 'foreground';
  4216. frame.appendChild(foreground);
  4217. this.dom.foreground = foreground;
  4218. // create axis panel
  4219. var axis = document.createElement('div');
  4220. axis.className = 'axis';
  4221. this.dom.axis = axis;
  4222. this.axisPanel.frame.appendChild(axis);
  4223. // attach event listeners
  4224. // TODO: use event listeners from the rootpanel to improve performance?
  4225. this.hammer = Hammer(frame, {
  4226. prevent_default: true
  4227. });
  4228. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4229. this.hammer.on('drag', this._onDrag.bind(this));
  4230. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4231. };
  4232. /**
  4233. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4234. * @param {Object} [options] The following options are available:
  4235. * {String | function} [className]
  4236. * class name for the itemset
  4237. * {String} [type]
  4238. * Default type for the items. Choose from 'box'
  4239. * (default), 'point', or 'range'. The default
  4240. * Style can be overwritten by individual items.
  4241. * {String} align
  4242. * Alignment for the items, only applicable for
  4243. * ItemBox. Choose 'center' (default), 'left', or
  4244. * 'right'.
  4245. * {String} orientation
  4246. * Orientation of the item set. Choose 'top' or
  4247. * 'bottom' (default).
  4248. * {Number} margin.axis
  4249. * Margin between the axis and the items in pixels.
  4250. * Default is 20.
  4251. * {Number} margin.item
  4252. * Margin between items in pixels. Default is 10.
  4253. * {Number} padding
  4254. * Padding of the contents of an item in pixels.
  4255. * Must correspond with the items css. Default is 5.
  4256. * {Function} snap
  4257. * Function to let items snap to nice dates when
  4258. * dragging items.
  4259. */
  4260. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4261. /**
  4262. * Hide the component from the DOM
  4263. */
  4264. ItemSet.prototype.hide = function hide() {
  4265. // remove the axis with dots
  4266. if (this.dom.axis.parentNode) {
  4267. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4268. }
  4269. // remove the background with vertical lines
  4270. if (this.dom.background.parentNode) {
  4271. this.dom.background.parentNode.removeChild(this.dom.background);
  4272. }
  4273. };
  4274. /**
  4275. * Show the component in the DOM (when not already visible).
  4276. * @return {Boolean} changed
  4277. */
  4278. ItemSet.prototype.show = function show() {
  4279. // show axis with dots
  4280. if (!this.dom.axis.parentNode) {
  4281. this.axisPanel.frame.appendChild(this.dom.axis);
  4282. }
  4283. // show background with vertical lines
  4284. if (!this.dom.background.parentNode) {
  4285. this.backgroundPanel.frame.appendChild(this.dom.background);
  4286. }
  4287. };
  4288. /**
  4289. * Set range (start and end).
  4290. * @param {Range | Object} range A Range or an object containing start and end.
  4291. */
  4292. ItemSet.prototype.setRange = function setRange(range) {
  4293. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4294. throw new TypeError('Range must be an instance of Range, ' +
  4295. 'or an object containing start and end.');
  4296. }
  4297. this.range = range;
  4298. };
  4299. /**
  4300. * Set selected items by their id. Replaces the current selection
  4301. * Unknown id's are silently ignored.
  4302. * @param {Array} [ids] An array with zero or more id's of the items to be
  4303. * selected. If ids is an empty array, all items will be
  4304. * unselected.
  4305. */
  4306. ItemSet.prototype.setSelection = function setSelection(ids) {
  4307. var i, ii, id, item;
  4308. if (ids) {
  4309. if (!Array.isArray(ids)) {
  4310. throw new TypeError('Array expected');
  4311. }
  4312. // unselect currently selected items
  4313. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4314. id = this.selection[i];
  4315. item = this.items[id];
  4316. if (item) item.unselect();
  4317. }
  4318. // select items
  4319. this.selection = [];
  4320. for (i = 0, ii = ids.length; i < ii; i++) {
  4321. id = ids[i];
  4322. item = this.items[id];
  4323. if (item) {
  4324. this.selection.push(id);
  4325. item.select();
  4326. }
  4327. }
  4328. }
  4329. };
  4330. /**
  4331. * Get the selected items by their id
  4332. * @return {Array} ids The ids of the selected items
  4333. */
  4334. ItemSet.prototype.getSelection = function getSelection() {
  4335. return this.selection.concat([]);
  4336. };
  4337. /**
  4338. * Deselect a selected item
  4339. * @param {String | Number} id
  4340. * @private
  4341. */
  4342. ItemSet.prototype._deselect = function _deselect(id) {
  4343. var selection = this.selection;
  4344. for (var i = 0, ii = selection.length; i < ii; i++) {
  4345. if (selection[i] == id) { // non-strict comparison!
  4346. selection.splice(i, 1);
  4347. break;
  4348. }
  4349. }
  4350. };
  4351. /**
  4352. * Return the item sets frame
  4353. * @returns {HTMLElement} frame
  4354. */
  4355. ItemSet.prototype.getFrame = function getFrame() {
  4356. return this.frame;
  4357. };
  4358. /**
  4359. * Repaint the component
  4360. * @return {boolean} Returns true if the component is resized
  4361. */
  4362. ItemSet.prototype.repaint = function repaint() {
  4363. var asSize = util.option.asSize,
  4364. asString = util.option.asString,
  4365. options = this.options,
  4366. orientation = this.getOption('orientation'),
  4367. frame = this.frame;
  4368. // update className
  4369. frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
  4370. // check whether zoomed (in that case we need to re-stack everything)
  4371. var visibleInterval = this.range.end - this.range.start;
  4372. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  4373. this.lastVisibleInterval = visibleInterval;
  4374. this.lastWidth = this.width;
  4375. /* TODO: implement+fix smarter way to update visible items
  4376. // find the first visible item
  4377. // TODO: use faster search, not linear
  4378. var byEnd = this.orderedItems.byEnd;
  4379. var start = 0;
  4380. var item = null;
  4381. while ((item = byEnd[start]) &&
  4382. (('end' in item.data) ? item.data.end : item.data.start) < this.range.start) {
  4383. start++;
  4384. }
  4385. // find the last visible item
  4386. // TODO: use faster search, not linear
  4387. var byStart = this.orderedItems.byStart;
  4388. var end = 0;
  4389. while ((item = byStart[end]) && item.data.start < this.range.end) {
  4390. end++;
  4391. }
  4392. console.log('visible items', start, end); // TODO: cleanup
  4393. console.log('visible item ids', byStart[start] && byStart[start].id, byEnd[end-1] && byEnd[end-1].id); // TODO: cleanup
  4394. this.visibleItems = [];
  4395. var i = start;
  4396. item = byStart[i];
  4397. var lastItem = byEnd[end];
  4398. while (item && item !== lastItem) {
  4399. this.visibleItems.push(item);
  4400. item = byStart[++i];
  4401. }
  4402. this.stack.order(this.visibleItems);
  4403. // show visible items
  4404. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  4405. item = this.visibleItems[i];
  4406. if (!item.displayed) item.show();
  4407. item.top = null; // reset stacking position
  4408. // reposition item horizontally
  4409. item.repositionX();
  4410. }
  4411. */
  4412. // simple, brute force calculation of visible items
  4413. // TODO: replace with a faster, more sophisticated solution
  4414. this.visibleItems = [];
  4415. for (var id in this.items) {
  4416. if (this.items.hasOwnProperty(id)) {
  4417. var item = this.items[id];
  4418. if (item.isVisible(this.range)) {
  4419. if (!item.displayed) item.show();
  4420. // reposition item horizontally
  4421. item.repositionX();
  4422. this.visibleItems.push(item);
  4423. }
  4424. else {
  4425. if (item.displayed) item.hide();
  4426. }
  4427. }
  4428. }
  4429. // reposition visible items vertically
  4430. //this.stack.order(this.visibleItems); // TODO: improve ordering
  4431. var force = this.stackDirty || zoomed; // force re-stacking of all items if true
  4432. this.stack.stack(this.visibleItems, force);
  4433. this.stackDirty = false;
  4434. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  4435. this.visibleItems[i].repositionY();
  4436. }
  4437. // recalculate the height of the itemset
  4438. var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis,
  4439. marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item,
  4440. height;
  4441. // determine the height from the stacked items
  4442. var visibleItems = this.visibleItems;
  4443. if (visibleItems.length) {
  4444. var min = visibleItems[0].top;
  4445. var max = visibleItems[0].top + visibleItems[0].height;
  4446. util.forEach(visibleItems, function (item) {
  4447. min = Math.min(min, item.top);
  4448. max = Math.max(max, (item.top + item.height));
  4449. });
  4450. height = (max - min) + marginAxis + marginItem;
  4451. }
  4452. else {
  4453. height = marginAxis + marginItem;
  4454. }
  4455. // reposition frame
  4456. frame.style.left = asSize(options.left, '');
  4457. frame.style.right = asSize(options.right, '');
  4458. frame.style.top = asSize((orientation == 'top') ? '0' : '');
  4459. frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4460. frame.style.width = asSize(options.width, '100%');
  4461. frame.style.height = asSize(height);
  4462. //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
  4463. // calculate actual size and position
  4464. this.top = frame.offsetTop;
  4465. this.left = frame.offsetLeft;
  4466. this.width = frame.offsetWidth;
  4467. this.height = height;
  4468. // reposition axis
  4469. this.dom.axis.style.left = asSize(options.left, '0');
  4470. this.dom.axis.style.right = asSize(options.right, '');
  4471. this.dom.axis.style.width = asSize(options.width, '100%');
  4472. this.dom.axis.style.height = asSize(0);
  4473. this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
  4474. this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4475. return this._isResized();
  4476. };
  4477. /**
  4478. * Get the foreground container element
  4479. * @return {HTMLElement} foreground
  4480. */
  4481. ItemSet.prototype.getForeground = function getForeground() {
  4482. return this.dom.foreground;
  4483. };
  4484. /**
  4485. * Get the background container element
  4486. * @return {HTMLElement} background
  4487. */
  4488. ItemSet.prototype.getBackground = function getBackground() {
  4489. return this.dom.background;
  4490. };
  4491. /**
  4492. * Get the axis container element
  4493. * @return {HTMLElement} axis
  4494. */
  4495. ItemSet.prototype.getAxis = function getAxis() {
  4496. return this.dom.axis;
  4497. };
  4498. /**
  4499. * Set items
  4500. * @param {vis.DataSet | null} items
  4501. */
  4502. ItemSet.prototype.setItems = function setItems(items) {
  4503. var me = this,
  4504. ids,
  4505. oldItemsData = this.itemsData;
  4506. // replace the dataset
  4507. if (!items) {
  4508. this.itemsData = null;
  4509. }
  4510. else if (items instanceof DataSet || items instanceof DataView) {
  4511. this.itemsData = items;
  4512. }
  4513. else {
  4514. throw new TypeError('Data must be an instance of DataSet');
  4515. }
  4516. if (oldItemsData) {
  4517. // unsubscribe from old dataset
  4518. util.forEach(this.listeners, function (callback, event) {
  4519. oldItemsData.unsubscribe(event, callback);
  4520. });
  4521. // remove all drawn items
  4522. ids = oldItemsData.getIds();
  4523. this._onRemove(ids);
  4524. }
  4525. if (this.itemsData) {
  4526. // subscribe to new dataset
  4527. var id = this.id;
  4528. util.forEach(this.listeners, function (callback, event) {
  4529. me.itemsData.on(event, callback, id);
  4530. });
  4531. // draw all new items
  4532. ids = this.itemsData.getIds();
  4533. this._onAdd(ids);
  4534. }
  4535. };
  4536. /**
  4537. * Get the current items items
  4538. * @returns {vis.DataSet | null}
  4539. */
  4540. ItemSet.prototype.getItems = function getItems() {
  4541. return this.itemsData;
  4542. };
  4543. /**
  4544. * Remove an item by its id
  4545. * @param {String | Number} id
  4546. */
  4547. ItemSet.prototype.removeItem = function removeItem (id) {
  4548. var item = this.itemsData.get(id),
  4549. dataset = this._myDataSet();
  4550. if (item) {
  4551. // confirm deletion
  4552. this.options.onRemove(item, function (item) {
  4553. if (item) {
  4554. // remove by id here, it is possible that an item has no id defined
  4555. // itself, so better not delete by the item itself
  4556. dataset.remove(id);
  4557. }
  4558. });
  4559. }
  4560. };
  4561. /**
  4562. * Handle updated items
  4563. * @param {Number[]} ids
  4564. * @private
  4565. */
  4566. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4567. var me = this,
  4568. items = this.items,
  4569. itemOptions = this.itemOptions;
  4570. ids.forEach(function (id) {
  4571. var itemData = me.itemsData.get(id),
  4572. item = items[id],
  4573. type = itemData.type ||
  4574. (itemData.start && itemData.end && 'range') ||
  4575. me.options.type ||
  4576. 'box';
  4577. var constructor = ItemSet.types[type];
  4578. if (item) {
  4579. // update item
  4580. if (!constructor || !(item instanceof constructor)) {
  4581. // item type has changed, hide and delete the item
  4582. item.hide();
  4583. item = null;
  4584. }
  4585. else {
  4586. item.data = itemData; // TODO: create a method item.setData ?
  4587. }
  4588. }
  4589. if (!item) {
  4590. // create item
  4591. if (constructor) {
  4592. item = new constructor(me, itemData, me.options, itemOptions);
  4593. item.id = id;
  4594. }
  4595. else {
  4596. throw new TypeError('Unknown item type "' + type + '"');
  4597. }
  4598. }
  4599. me.items[id] = item;
  4600. });
  4601. this._order();
  4602. this.stackDirty = true; // force re-stacking of all items next repaint
  4603. this.emit('change');
  4604. };
  4605. /**
  4606. * Handle added items
  4607. * @param {Number[]} ids
  4608. * @private
  4609. */
  4610. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  4611. /**
  4612. * Handle removed items
  4613. * @param {Number[]} ids
  4614. * @private
  4615. */
  4616. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4617. var count = 0;
  4618. var me = this;
  4619. ids.forEach(function (id) {
  4620. var item = me.items[id];
  4621. if (item) {
  4622. count++;
  4623. item.hide();
  4624. delete me.items[id];
  4625. delete me.visibleItems[id];
  4626. // remove from selection
  4627. var index = me.selection.indexOf(id);
  4628. if (index != -1) me.selection.splice(index, 1);
  4629. }
  4630. });
  4631. if (count) {
  4632. // update order
  4633. this._order();
  4634. this.stackDirty = true; // force re-stacking of all items next repaint
  4635. this.emit('change');
  4636. }
  4637. };
  4638. /**
  4639. * Order the items
  4640. * @private
  4641. */
  4642. ItemSet.prototype._order = function _order() {
  4643. var array = util.toArray(this.items);
  4644. this.orderedItems.byStart = array;
  4645. this.orderedItems.byEnd = [].concat(array);
  4646. // reorder the items
  4647. this.stack.orderByStart(this.orderedItems.byStart);
  4648. this.stack.orderByEnd(this.orderedItems.byEnd);
  4649. };
  4650. /**
  4651. * Start dragging the selected events
  4652. * @param {Event} event
  4653. * @private
  4654. */
  4655. ItemSet.prototype._onDragStart = function (event) {
  4656. if (!this.options.editable) {
  4657. return;
  4658. }
  4659. var item = ItemSet.itemFromTarget(event),
  4660. me = this;
  4661. if (item && item.selected) {
  4662. var dragLeftItem = event.target.dragLeftItem;
  4663. var dragRightItem = event.target.dragRightItem;
  4664. if (dragLeftItem) {
  4665. this.touchParams.itemProps = [{
  4666. item: dragLeftItem,
  4667. start: item.data.start.valueOf()
  4668. }];
  4669. }
  4670. else if (dragRightItem) {
  4671. this.touchParams.itemProps = [{
  4672. item: dragRightItem,
  4673. end: item.data.end.valueOf()
  4674. }];
  4675. }
  4676. else {
  4677. this.touchParams.itemProps = this.getSelection().map(function (id) {
  4678. var item = me.items[id];
  4679. var props = {
  4680. item: item
  4681. };
  4682. if ('start' in item.data) {
  4683. props.start = item.data.start.valueOf()
  4684. }
  4685. if ('end' in item.data) {
  4686. props.end = item.data.end.valueOf()
  4687. }
  4688. return props;
  4689. });
  4690. }
  4691. event.stopPropagation();
  4692. }
  4693. };
  4694. /**
  4695. * Drag selected items
  4696. * @param {Event} event
  4697. * @private
  4698. */
  4699. ItemSet.prototype._onDrag = function (event) {
  4700. if (this.touchParams.itemProps) {
  4701. var snap = this.options.snap || null,
  4702. deltaX = event.gesture.deltaX,
  4703. scale = (this.width / (this.range.end - this.range.start)),
  4704. offset = deltaX / scale;
  4705. // move
  4706. this.touchParams.itemProps.forEach(function (props) {
  4707. if ('start' in props) {
  4708. var start = new Date(props.start + offset);
  4709. props.item.data.start = snap ? snap(start) : start;
  4710. }
  4711. if ('end' in props) {
  4712. var end = new Date(props.end + offset);
  4713. props.item.data.end = snap ? snap(end) : end;
  4714. }
  4715. });
  4716. // TODO: implement onMoving handler
  4717. // TODO: implement dragging from one group to another
  4718. this.stackDirty = true; // force re-stacking of all items next repaint
  4719. this.emit('change');
  4720. event.stopPropagation();
  4721. }
  4722. };
  4723. /**
  4724. * End of dragging selected items
  4725. * @param {Event} event
  4726. * @private
  4727. */
  4728. ItemSet.prototype._onDragEnd = function (event) {
  4729. if (this.touchParams.itemProps) {
  4730. // prepare a change set for the changed items
  4731. var changes = [],
  4732. me = this,
  4733. dataset = this._myDataSet();
  4734. this.touchParams.itemProps.forEach(function (props) {
  4735. var id = props.item.id,
  4736. item = me.itemsData.get(id);
  4737. var changed = false;
  4738. if ('start' in props.item.data) {
  4739. changed = (props.start != props.item.data.start.valueOf());
  4740. item.start = util.convert(props.item.data.start, dataset.convert['start']);
  4741. }
  4742. if ('end' in props.item.data) {
  4743. changed = changed || (props.end != props.item.data.end.valueOf());
  4744. item.end = util.convert(props.item.data.end, dataset.convert['end']);
  4745. }
  4746. // only apply changes when start or end is actually changed
  4747. if (changed) {
  4748. me.options.onMove(item, function (item) {
  4749. if (item) {
  4750. // apply changes
  4751. item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
  4752. changes.push(item);
  4753. }
  4754. else {
  4755. // restore original values
  4756. if ('start' in props) props.item.data.start = props.start;
  4757. if ('end' in props) props.item.data.end = props.end;
  4758. me.stackDirty = true; // force re-stacking of all items next repaint
  4759. me.emit('change');
  4760. }
  4761. });
  4762. }
  4763. });
  4764. this.touchParams.itemProps = null;
  4765. // apply the changes to the data (if there are changes)
  4766. if (changes.length) {
  4767. dataset.update(changes);
  4768. }
  4769. event.stopPropagation();
  4770. }
  4771. };
  4772. /**
  4773. * Find an item from an event target:
  4774. * searches for the attribute 'timeline-item' in the event target's element tree
  4775. * @param {Event} event
  4776. * @return {Item | null} item
  4777. */
  4778. ItemSet.itemFromTarget = function itemFromTarget (event) {
  4779. var target = event.target;
  4780. while (target) {
  4781. if (target.hasOwnProperty('timeline-item')) {
  4782. return target['timeline-item'];
  4783. }
  4784. target = target.parentNode;
  4785. }
  4786. return null;
  4787. };
  4788. /**
  4789. * Find the ItemSet from an event target:
  4790. * searches for the attribute 'timeline-itemset' in the event target's element tree
  4791. * @param {Event} event
  4792. * @return {ItemSet | null} item
  4793. */
  4794. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  4795. var target = event.target;
  4796. while (target) {
  4797. if (target.hasOwnProperty('timeline-itemset')) {
  4798. return target['timeline-itemset'];
  4799. }
  4800. target = target.parentNode;
  4801. }
  4802. return null;
  4803. };
  4804. /**
  4805. * Find the DataSet to which this ItemSet is connected
  4806. * @returns {null | DataSet} dataset
  4807. * @private
  4808. */
  4809. ItemSet.prototype._myDataSet = function _myDataSet() {
  4810. // find the root DataSet
  4811. var dataset = this.itemsData;
  4812. while (dataset instanceof DataView) {
  4813. dataset = dataset.data;
  4814. }
  4815. return dataset;
  4816. };
  4817. /**
  4818. * @constructor Item
  4819. * @param {ItemSet} parent
  4820. * @param {Object} data Object containing (optional) parameters type,
  4821. * start, end, content, group, className.
  4822. * @param {Object} [options] Options to set initial property values
  4823. * @param {Object} [defaultOptions] default options
  4824. * // TODO: describe available options
  4825. */
  4826. function Item (parent, data, options, defaultOptions) {
  4827. this.parent = parent;
  4828. this.data = data;
  4829. this.dom = null;
  4830. this.options = options || {};
  4831. this.defaultOptions = defaultOptions || {};
  4832. this.selected = false;
  4833. this.displayed = false;
  4834. this.dirty = true;
  4835. this.top = null;
  4836. this.left = null;
  4837. this.width = null;
  4838. this.height = null;
  4839. }
  4840. /**
  4841. * Select current item
  4842. */
  4843. Item.prototype.select = function select() {
  4844. this.selected = true;
  4845. if (this.displayed) this.repaint();
  4846. };
  4847. /**
  4848. * Unselect current item
  4849. */
  4850. Item.prototype.unselect = function unselect() {
  4851. this.selected = false;
  4852. if (this.displayed) this.repaint();
  4853. };
  4854. /**
  4855. * Show the Item in the DOM (when not already visible)
  4856. * @return {Boolean} changed
  4857. */
  4858. Item.prototype.show = function show() {
  4859. return false;
  4860. };
  4861. /**
  4862. * Hide the Item from the DOM (when visible)
  4863. * @return {Boolean} changed
  4864. */
  4865. Item.prototype.hide = function hide() {
  4866. return false;
  4867. };
  4868. /**
  4869. * Repaint the item
  4870. */
  4871. Item.prototype.repaint = function repaint() {
  4872. // should be implemented by the item
  4873. };
  4874. /**
  4875. * Reposition the Item horizontally
  4876. */
  4877. Item.prototype.repositionX = function repositionX() {
  4878. // should be implemented by the item
  4879. };
  4880. /**
  4881. * Reposition the Item vertically
  4882. */
  4883. Item.prototype.repositionY = function repositionY() {
  4884. // should be implemented by the item
  4885. };
  4886. /**
  4887. * Repaint a delete button on the top right of the item when the item is selected
  4888. * @param {HTMLElement} anchor
  4889. * @private
  4890. */
  4891. Item.prototype._repaintDeleteButton = function (anchor) {
  4892. if (this.selected && this.options.editable && !this.dom.deleteButton) {
  4893. // create and show button
  4894. var parent = this.parent;
  4895. var id = this.id;
  4896. var deleteButton = document.createElement('div');
  4897. deleteButton.className = 'delete';
  4898. deleteButton.title = 'Delete this item';
  4899. Hammer(deleteButton, {
  4900. preventDefault: true
  4901. }).on('tap', function (event) {
  4902. parent.removeItem(id);
  4903. event.stopPropagation();
  4904. });
  4905. anchor.appendChild(deleteButton);
  4906. this.dom.deleteButton = deleteButton;
  4907. }
  4908. else if (!this.selected && this.dom.deleteButton) {
  4909. // remove button
  4910. if (this.dom.deleteButton.parentNode) {
  4911. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  4912. }
  4913. this.dom.deleteButton = null;
  4914. }
  4915. };
  4916. /**
  4917. * @constructor ItemBox
  4918. * @extends Item
  4919. * @param {ItemSet} parent
  4920. * @param {Object} data Object containing parameters start
  4921. * content, className.
  4922. * @param {Object} [options] Options to set initial property values
  4923. * @param {Object} [defaultOptions] default options
  4924. * // TODO: describe available options
  4925. */
  4926. function ItemBox (parent, data, options, defaultOptions) {
  4927. this.props = {
  4928. dot: {
  4929. width: 0,
  4930. height: 0
  4931. },
  4932. line: {
  4933. width: 0,
  4934. height: 0
  4935. }
  4936. };
  4937. // validate data
  4938. if (data) {
  4939. if (data.start == undefined) {
  4940. throw new Error('Property "start" missing in item ' + data);
  4941. }
  4942. }
  4943. Item.call(this, parent, data, options, defaultOptions);
  4944. }
  4945. ItemBox.prototype = new Item (null, null);
  4946. /**
  4947. * Check whether this item is visible inside given range
  4948. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  4949. * @returns {boolean} True if visible
  4950. */
  4951. ItemBox.prototype.isVisible = function isVisible (range) {
  4952. // determine visibility
  4953. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  4954. var interval = (range.end - range.start) / 4;
  4955. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  4956. };
  4957. /**
  4958. * Repaint the item
  4959. */
  4960. ItemBox.prototype.repaint = function repaint() {
  4961. var dom = this.dom;
  4962. if (!dom) {
  4963. // create DOM
  4964. this.dom = {};
  4965. dom = this.dom;
  4966. // create main box
  4967. dom.box = document.createElement('DIV');
  4968. // contents box (inside the background box). used for making margins
  4969. dom.content = document.createElement('DIV');
  4970. dom.content.className = 'content';
  4971. dom.box.appendChild(dom.content);
  4972. // line to axis
  4973. dom.line = document.createElement('DIV');
  4974. dom.line.className = 'line';
  4975. // dot on axis
  4976. dom.dot = document.createElement('DIV');
  4977. dom.dot.className = 'dot';
  4978. // attach this item as attribute
  4979. dom.box['timeline-item'] = this;
  4980. }
  4981. // append DOM to parent DOM
  4982. if (!this.parent) {
  4983. throw new Error('Cannot repaint item: no parent attached');
  4984. }
  4985. if (!dom.box.parentNode) {
  4986. var foreground = this.parent.getForeground();
  4987. if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
  4988. foreground.appendChild(dom.box);
  4989. }
  4990. if (!dom.line.parentNode) {
  4991. var background = this.parent.getBackground();
  4992. if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
  4993. background.appendChild(dom.line);
  4994. }
  4995. if (!dom.dot.parentNode) {
  4996. var axis = this.parent.getAxis();
  4997. if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
  4998. axis.appendChild(dom.dot);
  4999. }
  5000. this.displayed = true;
  5001. // update contents
  5002. if (this.data.content != this.content) {
  5003. this.content = this.data.content;
  5004. if (this.content instanceof Element) {
  5005. dom.content.innerHTML = '';
  5006. dom.content.appendChild(this.content);
  5007. }
  5008. else if (this.data.content != undefined) {
  5009. dom.content.innerHTML = this.content;
  5010. }
  5011. else {
  5012. throw new Error('Property "content" missing in item ' + this.data.id);
  5013. }
  5014. this.dirty = true;
  5015. }
  5016. // update class
  5017. var className = (this.data.className? ' ' + this.data.className : '') +
  5018. (this.selected ? ' selected' : '');
  5019. if (this.className != className) {
  5020. this.className = className;
  5021. dom.box.className = 'item box' + className;
  5022. dom.line.className = 'item line' + className;
  5023. dom.dot.className = 'item dot' + className;
  5024. this.dirty = true;
  5025. }
  5026. // recalculate size
  5027. if (this.dirty) {
  5028. this.props.dot.height = dom.dot.offsetHeight;
  5029. this.props.dot.width = dom.dot.offsetWidth;
  5030. this.props.line.width = dom.line.offsetWidth;
  5031. this.width = dom.box.offsetWidth;
  5032. this.height = dom.box.offsetHeight;
  5033. this.dirty = false;
  5034. }
  5035. this._repaintDeleteButton(dom.box);
  5036. };
  5037. /**
  5038. * Show the item in the DOM (when not already displayed). The items DOM will
  5039. * be created when needed.
  5040. */
  5041. ItemBox.prototype.show = function show() {
  5042. if (!this.displayed) {
  5043. this.repaint();
  5044. }
  5045. };
  5046. /**
  5047. * Hide the item from the DOM (when visible)
  5048. */
  5049. ItemBox.prototype.hide = function hide() {
  5050. if (this.displayed) {
  5051. var dom = this.dom;
  5052. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  5053. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  5054. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  5055. this.top = null;
  5056. this.left = null;
  5057. this.displayed = false;
  5058. }
  5059. };
  5060. /**
  5061. * Reposition the item horizontally
  5062. * @Override
  5063. */
  5064. ItemBox.prototype.repositionX = function repositionX() {
  5065. var start = this.defaultOptions.toScreen(this.data.start),
  5066. align = this.options.align || this.defaultOptions.align,
  5067. left,
  5068. box = this.dom.box,
  5069. line = this.dom.line,
  5070. dot = this.dom.dot;
  5071. // calculate left position of the box
  5072. if (align == 'right') {
  5073. this.left = start - this.width;
  5074. }
  5075. else if (align == 'left') {
  5076. this.left = start;
  5077. }
  5078. else {
  5079. // default or 'center'
  5080. this.left = start - this.width / 2;
  5081. }
  5082. // reposition box
  5083. box.style.left = this.left + 'px';
  5084. // reposition line
  5085. line.style.left = (start - this.props.line.width / 2) + 'px';
  5086. // reposition dot
  5087. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  5088. };
  5089. /**
  5090. * Reposition the item vertically
  5091. * @Override
  5092. */
  5093. ItemBox.prototype.repositionY = function repositionY () {
  5094. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5095. box = this.dom.box,
  5096. line = this.dom.line,
  5097. dot = this.dom.dot;
  5098. if (orientation == 'top') {
  5099. box.style.top = (this.top || 0) + 'px';
  5100. box.style.bottom = '';
  5101. line.style.top = '0';
  5102. line.style.bottom = '';
  5103. line.style.height = (this.parent.top + this.top + 1) + 'px';
  5104. }
  5105. else { // orientation 'bottom'
  5106. box.style.top = '';
  5107. box.style.bottom = (this.top || 0) + 'px';
  5108. line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
  5109. line.style.bottom = '0';
  5110. line.style.height = '';
  5111. }
  5112. dot.style.top = (-this.props.dot.height / 2) + 'px';
  5113. };
  5114. /**
  5115. * @constructor ItemPoint
  5116. * @extends Item
  5117. * @param {ItemSet} parent
  5118. * @param {Object} data Object containing parameters start
  5119. * content, className.
  5120. * @param {Object} [options] Options to set initial property values
  5121. * @param {Object} [defaultOptions] default options
  5122. * // TODO: describe available options
  5123. */
  5124. function ItemPoint (parent, data, options, defaultOptions) {
  5125. this.props = {
  5126. dot: {
  5127. top: 0,
  5128. width: 0,
  5129. height: 0
  5130. },
  5131. content: {
  5132. height: 0,
  5133. marginLeft: 0
  5134. }
  5135. };
  5136. // validate data
  5137. if (data) {
  5138. if (data.start == undefined) {
  5139. throw new Error('Property "start" missing in item ' + data);
  5140. }
  5141. }
  5142. Item.call(this, parent, data, options, defaultOptions);
  5143. }
  5144. ItemPoint.prototype = new Item (null, null);
  5145. /**
  5146. * Check whether this item is visible inside given range
  5147. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5148. * @returns {boolean} True if visible
  5149. */
  5150. ItemPoint.prototype.isVisible = function isVisible (range) {
  5151. // determine visibility
  5152. var interval = (range.end - range.start);
  5153. return (this.data.start > range.start - interval) && (this.data.start < range.end);
  5154. }
  5155. /**
  5156. * Repaint the item
  5157. */
  5158. ItemPoint.prototype.repaint = function repaint() {
  5159. var dom = this.dom;
  5160. if (!dom) {
  5161. // create DOM
  5162. this.dom = {};
  5163. dom = this.dom;
  5164. // background box
  5165. dom.point = document.createElement('div');
  5166. // className is updated in repaint()
  5167. // contents box, right from the dot
  5168. dom.content = document.createElement('div');
  5169. dom.content.className = 'content';
  5170. dom.point.appendChild(dom.content);
  5171. // dot at start
  5172. dom.dot = document.createElement('div');
  5173. dom.dot.className = 'dot';
  5174. dom.point.appendChild(dom.dot);
  5175. // attach this item as attribute
  5176. dom.point['timeline-item'] = this;
  5177. }
  5178. // append DOM to parent DOM
  5179. if (!this.parent) {
  5180. throw new Error('Cannot repaint item: no parent attached');
  5181. }
  5182. if (!dom.point.parentNode) {
  5183. var foreground = this.parent.getForeground();
  5184. if (!foreground) {
  5185. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5186. }
  5187. foreground.appendChild(dom.point);
  5188. }
  5189. this.displayed = true;
  5190. // update contents
  5191. if (this.data.content != this.content) {
  5192. this.content = this.data.content;
  5193. if (this.content instanceof Element) {
  5194. dom.content.innerHTML = '';
  5195. dom.content.appendChild(this.content);
  5196. }
  5197. else if (this.data.content != undefined) {
  5198. dom.content.innerHTML = this.content;
  5199. }
  5200. else {
  5201. throw new Error('Property "content" missing in item ' + this.data.id);
  5202. }
  5203. this.dirty = true;
  5204. }
  5205. // update class
  5206. var className = (this.data.className? ' ' + this.data.className : '') +
  5207. (this.selected ? ' selected' : '');
  5208. if (this.className != className) {
  5209. this.className = className;
  5210. dom.point.className = 'item point' + className;
  5211. this.dirty = true;
  5212. }
  5213. // recalculate size
  5214. if (this.dirty) {
  5215. this.width = dom.point.offsetWidth;
  5216. this.height = dom.point.offsetHeight;
  5217. this.props.dot.width = dom.dot.offsetWidth;
  5218. this.props.dot.height = dom.dot.offsetHeight;
  5219. this.props.content.height = dom.content.offsetHeight;
  5220. // resize contents
  5221. dom.content.style.marginLeft = 1.5 * this.props.dot.width + 'px';
  5222. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  5223. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  5224. this.dirty = false;
  5225. }
  5226. this._repaintDeleteButton(dom.point);
  5227. };
  5228. /**
  5229. * Show the item in the DOM (when not already visible). The items DOM will
  5230. * be created when needed.
  5231. */
  5232. ItemPoint.prototype.show = function show() {
  5233. if (!this.displayed) {
  5234. this.repaint();
  5235. }
  5236. };
  5237. /**
  5238. * Hide the item from the DOM (when visible)
  5239. */
  5240. ItemPoint.prototype.hide = function hide() {
  5241. if (this.displayed) {
  5242. if (this.dom.point.parentNode) {
  5243. this.dom.point.parentNode.removeChild(this.dom.point);
  5244. }
  5245. this.top = null;
  5246. this.left = null;
  5247. this.displayed = false;
  5248. }
  5249. };
  5250. /**
  5251. * Reposition the item horizontally
  5252. * @Override
  5253. */
  5254. ItemPoint.prototype.repositionX = function repositionX() {
  5255. var start = this.defaultOptions.toScreen(this.data.start);
  5256. this.left = start - this.props.dot.width / 2;
  5257. // reposition point
  5258. this.dom.point.style.left = this.left + 'px';
  5259. };
  5260. /**
  5261. * Reposition the item vertically
  5262. * @Override
  5263. */
  5264. ItemPoint.prototype.repositionY = function repositionY () {
  5265. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5266. point = this.dom.point;
  5267. if (orientation == 'top') {
  5268. point.style.top = this.top + 'px';
  5269. point.style.bottom = '';
  5270. }
  5271. else {
  5272. point.style.top = '';
  5273. point.style.bottom = this.top + 'px';
  5274. }
  5275. }
  5276. /**
  5277. * @constructor ItemRange
  5278. * @extends Item
  5279. * @param {ItemSet} parent
  5280. * @param {Object} data Object containing parameters start, end
  5281. * content, className.
  5282. * @param {Object} [options] Options to set initial property values
  5283. * @param {Object} [defaultOptions] default options
  5284. * // TODO: describe available options
  5285. */
  5286. function ItemRange (parent, data, options, defaultOptions) {
  5287. this.props = {
  5288. content: {
  5289. width: 0
  5290. }
  5291. };
  5292. // validate data
  5293. if (data) {
  5294. if (data.start == undefined) {
  5295. throw new Error('Property "start" missing in item ' + data.id);
  5296. }
  5297. if (data.end == undefined) {
  5298. throw new Error('Property "end" missing in item ' + data.id);
  5299. }
  5300. }
  5301. Item.call(this, parent, data, options, defaultOptions);
  5302. }
  5303. ItemRange.prototype = new Item (null, null);
  5304. ItemRange.prototype.baseClassName = 'item range';
  5305. /**
  5306. * Check whether this item is visible inside given range
  5307. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5308. * @returns {boolean} True if visible
  5309. */
  5310. ItemRange.prototype.isVisible = function isVisible (range) {
  5311. // determine visibility
  5312. return (this.data.start < range.end) && (this.data.end > range.start);
  5313. };
  5314. /**
  5315. * Repaint the item
  5316. */
  5317. ItemRange.prototype.repaint = function repaint() {
  5318. var dom = this.dom;
  5319. if (!dom) {
  5320. // create DOM
  5321. this.dom = {};
  5322. dom = this.dom;
  5323. // background box
  5324. dom.box = document.createElement('div');
  5325. // className is updated in repaint()
  5326. // contents box
  5327. dom.content = document.createElement('div');
  5328. dom.content.className = 'content';
  5329. dom.box.appendChild(dom.content);
  5330. // attach this item as attribute
  5331. dom.box['timeline-item'] = this;
  5332. }
  5333. // append DOM to parent DOM
  5334. if (!this.parent) {
  5335. throw new Error('Cannot repaint item: no parent attached');
  5336. }
  5337. if (!dom.box.parentNode) {
  5338. var foreground = this.parent.getForeground();
  5339. if (!foreground) {
  5340. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5341. }
  5342. foreground.appendChild(dom.box);
  5343. }
  5344. this.displayed = true;
  5345. // update contents
  5346. if (this.data.content != this.content) {
  5347. this.content = this.data.content;
  5348. if (this.content instanceof Element) {
  5349. dom.content.innerHTML = '';
  5350. dom.content.appendChild(this.content);
  5351. }
  5352. else if (this.data.content != undefined) {
  5353. dom.content.innerHTML = this.content;
  5354. }
  5355. else {
  5356. throw new Error('Property "content" missing in item ' + this.data.id);
  5357. }
  5358. this.dirty = true;
  5359. }
  5360. // update class
  5361. var className = (this.data.className ? (' ' + this.data.className) : '') +
  5362. (this.selected ? ' selected' : '');
  5363. if (this.className != className) {
  5364. this.className = className;
  5365. dom.box.className = this.baseClassName + className;
  5366. this.dirty = true;
  5367. }
  5368. // recalculate size
  5369. if (this.dirty) {
  5370. this.props.content.width = this.dom.content.offsetWidth;
  5371. this.height = this.dom.box.offsetHeight;
  5372. this.dirty = false;
  5373. }
  5374. this._repaintDeleteButton(dom.box);
  5375. this._repaintDragLeft();
  5376. this._repaintDragRight();
  5377. };
  5378. /**
  5379. * Show the item in the DOM (when not already visible). The items DOM will
  5380. * be created when needed.
  5381. */
  5382. ItemRange.prototype.show = function show() {
  5383. if (!this.displayed) {
  5384. this.repaint();
  5385. }
  5386. };
  5387. /**
  5388. * Hide the item from the DOM (when visible)
  5389. * @return {Boolean} changed
  5390. */
  5391. ItemRange.prototype.hide = function hide() {
  5392. if (this.displayed) {
  5393. var box = this.dom.box;
  5394. if (box.parentNode) {
  5395. box.parentNode.removeChild(box);
  5396. }
  5397. this.top = null;
  5398. this.left = null;
  5399. this.displayed = false;
  5400. }
  5401. };
  5402. /**
  5403. * Reposition the item horizontally
  5404. * @Override
  5405. */
  5406. ItemRange.prototype.repositionX = function repositionX() {
  5407. var props = this.props,
  5408. parentWidth = this.parent.width,
  5409. start = this.defaultOptions.toScreen(this.data.start),
  5410. end = this.defaultOptions.toScreen(this.data.end),
  5411. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5412. contentLeft;
  5413. // limit the width of the this, as browsers cannot draw very wide divs
  5414. if (start < -parentWidth) {
  5415. start = -parentWidth;
  5416. }
  5417. if (end > 2 * parentWidth) {
  5418. end = 2 * parentWidth;
  5419. }
  5420. // when range exceeds left of the window, position the contents at the left of the visible area
  5421. if (start < 0) {
  5422. contentLeft = Math.min(-start,
  5423. (end - start - props.content.width - 2 * padding));
  5424. // TODO: remove the need for options.padding. it's terrible.
  5425. }
  5426. else {
  5427. contentLeft = 0;
  5428. }
  5429. this.left = start;
  5430. this.width = Math.max(end - start, 1);
  5431. this.dom.box.style.left = this.left + 'px';
  5432. this.dom.box.style.width = this.width + 'px';
  5433. this.dom.content.style.left = contentLeft + 'px';
  5434. };
  5435. /**
  5436. * Reposition the item vertically
  5437. * @Override
  5438. */
  5439. ItemRange.prototype.repositionY = function repositionY() {
  5440. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5441. box = this.dom.box;
  5442. if (orientation == 'top') {
  5443. box.style.top = this.top + 'px';
  5444. box.style.bottom = '';
  5445. }
  5446. else {
  5447. box.style.top = '';
  5448. box.style.bottom = this.top + 'px';
  5449. }
  5450. };
  5451. /**
  5452. * Repaint a drag area on the left side of the range when the range is selected
  5453. * @private
  5454. */
  5455. ItemRange.prototype._repaintDragLeft = function () {
  5456. if (this.selected && this.options.editable && !this.dom.dragLeft) {
  5457. // create and show drag area
  5458. var dragLeft = document.createElement('div');
  5459. dragLeft.className = 'drag-left';
  5460. dragLeft.dragLeftItem = this;
  5461. // TODO: this should be redundant?
  5462. Hammer(dragLeft, {
  5463. preventDefault: true
  5464. }).on('drag', function () {
  5465. //console.log('drag left')
  5466. });
  5467. this.dom.box.appendChild(dragLeft);
  5468. this.dom.dragLeft = dragLeft;
  5469. }
  5470. else if (!this.selected && this.dom.dragLeft) {
  5471. // delete drag area
  5472. if (this.dom.dragLeft.parentNode) {
  5473. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  5474. }
  5475. this.dom.dragLeft = null;
  5476. }
  5477. };
  5478. /**
  5479. * Repaint a drag area on the right side of the range when the range is selected
  5480. * @private
  5481. */
  5482. ItemRange.prototype._repaintDragRight = function () {
  5483. if (this.selected && this.options.editable && !this.dom.dragRight) {
  5484. // create and show drag area
  5485. var dragRight = document.createElement('div');
  5486. dragRight.className = 'drag-right';
  5487. dragRight.dragRightItem = this;
  5488. // TODO: this should be redundant?
  5489. Hammer(dragRight, {
  5490. preventDefault: true
  5491. }).on('drag', function () {
  5492. //console.log('drag right')
  5493. });
  5494. this.dom.box.appendChild(dragRight);
  5495. this.dom.dragRight = dragRight;
  5496. }
  5497. else if (!this.selected && this.dom.dragRight) {
  5498. // delete drag area
  5499. if (this.dom.dragRight.parentNode) {
  5500. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  5501. }
  5502. this.dom.dragRight = null;
  5503. }
  5504. };
  5505. /**
  5506. * @constructor ItemRangeOverflow
  5507. * @extends ItemRange
  5508. * @param {ItemSet} parent
  5509. * @param {Object} data Object containing parameters start, end
  5510. * content, className.
  5511. * @param {Object} [options] Options to set initial property values
  5512. * @param {Object} [defaultOptions] default options
  5513. * // TODO: describe available options
  5514. */
  5515. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  5516. this.props = {
  5517. content: {
  5518. left: 0,
  5519. width: 0
  5520. }
  5521. };
  5522. ItemRange.call(this, parent, data, options, defaultOptions);
  5523. }
  5524. ItemRangeOverflow.prototype = new ItemRange (null, null);
  5525. ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
  5526. /**
  5527. * Reposition the item horizontally
  5528. * @Override
  5529. */
  5530. ItemRangeOverflow.prototype.repositionX = function repositionX() {
  5531. var parentWidth = this.parent.width,
  5532. start = this.defaultOptions.toScreen(this.data.start),
  5533. end = this.defaultOptions.toScreen(this.data.end),
  5534. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5535. contentLeft;
  5536. // limit the width of the this, as browsers cannot draw very wide divs
  5537. if (start < -parentWidth) {
  5538. start = -parentWidth;
  5539. }
  5540. if (end > 2 * parentWidth) {
  5541. end = 2 * parentWidth;
  5542. }
  5543. // when range exceeds left of the window, position the contents at the left of the visible area
  5544. contentLeft = Math.max(-start, 0);
  5545. this.left = start;
  5546. var boxWidth = Math.max(end - start, 1);
  5547. this.width = (this.props.content.width < boxWidth) ?
  5548. boxWidth :
  5549. start + contentLeft + this.props.content.width;
  5550. this.dom.box.style.left = this.left + 'px';
  5551. this.dom.box.style.width = boxWidth + 'px';
  5552. this.dom.content.style.left = contentLeft + 'px';
  5553. };
  5554. /**
  5555. * @constructor Group
  5556. * @param {Panel} groupPanel
  5557. * @param {Panel} labelPanel
  5558. * @param {Panel} backgroundPanel
  5559. * @param {Panel} axisPanel
  5560. * @param {Number | String} groupId
  5561. * @param {Object} [options] Options to set initial property values
  5562. * // TODO: describe available options
  5563. * @extends Component
  5564. */
  5565. function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) {
  5566. this.id = util.randomUUID();
  5567. this.groupPanel = groupPanel;
  5568. this.labelPanel = labelPanel;
  5569. this.backgroundPanel = backgroundPanel;
  5570. this.axisPanel = axisPanel;
  5571. this.groupId = groupId;
  5572. this.itemSet = null; // ItemSet
  5573. this.options = options || {};
  5574. this.options.top = 0;
  5575. this.props = {
  5576. label: {
  5577. width: 0,
  5578. height: 0
  5579. }
  5580. };
  5581. this.dom = {};
  5582. this.top = 0;
  5583. this.left = 0;
  5584. this.width = 0;
  5585. this.height = 0;
  5586. this._create();
  5587. }
  5588. Group.prototype = new Component();
  5589. // TODO: comment
  5590. Group.prototype.setOptions = Component.prototype.setOptions;
  5591. /**
  5592. * Create DOM elements for the group
  5593. * @private
  5594. */
  5595. Group.prototype._create = function() {
  5596. var label = document.createElement('div');
  5597. label.className = 'vlabel';
  5598. this.dom.label = label;
  5599. var inner = document.createElement('div');
  5600. inner.className = 'inner';
  5601. label.appendChild(inner);
  5602. this.dom.inner = inner;
  5603. };
  5604. /**
  5605. * Set the group data for this group
  5606. * @param {Object} data Group data, can contain properties content and className
  5607. */
  5608. Group.prototype.setData = function setData(data) {
  5609. // update contents
  5610. var content = data && data.content;
  5611. if (content instanceof Element) {
  5612. this.dom.inner.appendChild(content);
  5613. }
  5614. else if (content != undefined) {
  5615. this.dom.inner.innerHTML = content;
  5616. }
  5617. else {
  5618. this.dom.inner.innerHTML = this.groupId;
  5619. }
  5620. // update className
  5621. var className = data && data.className;
  5622. if (className) {
  5623. util.addClassName(this.dom.label, className);
  5624. }
  5625. };
  5626. /**
  5627. * Set item set for the group. The group will create a view on the itemSet,
  5628. * filtered by the groups id.
  5629. * @param {DataSet | DataView} itemsData
  5630. */
  5631. Group.prototype.setItems = function setItems(itemsData) {
  5632. if (this.itemSet) {
  5633. // remove current item set
  5634. this.itemSet.setItems();
  5635. this.itemSet.hide();
  5636. this.groupPanel.frame.removeChild(this.itemSet.getFrame());
  5637. this.itemSet = null;
  5638. }
  5639. if (itemsData) {
  5640. var groupId = this.groupId;
  5641. var me = this;
  5642. var itemSetOptions = util.extend(this.options, {
  5643. height: function () {
  5644. // FIXME: setting height doesn't yet work
  5645. return Math.max(me.props.label.height, me.itemSet.height);
  5646. }
  5647. });
  5648. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions);
  5649. this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event
  5650. this.itemSet.parent = this;
  5651. this.groupPanel.frame.appendChild(this.itemSet.getFrame());
  5652. if (this.range) this.itemSet.setRange(this.range);
  5653. this.view = new DataView(itemsData, {
  5654. filter: function (item) {
  5655. return item.group == groupId;
  5656. }
  5657. });
  5658. this.itemSet.setItems(this.view);
  5659. }
  5660. };
  5661. /**
  5662. * hide the group, detach from DOM if needed
  5663. */
  5664. Group.prototype.show = function show() {
  5665. if (!this.dom.label.parentNode) {
  5666. this.labelPanel.frame.appendChild(this.dom.label);
  5667. }
  5668. var itemSetFrame = this.itemSet && this.itemSet.getFrame();
  5669. if (itemSetFrame) {
  5670. if (itemSetFrame.parentNode) {
  5671. itemSetFrame.parentNode.removeChild(itemSetFrame);
  5672. }
  5673. this.groupPanel.frame.appendChild(itemSetFrame);
  5674. this.itemSet.show();
  5675. }
  5676. };
  5677. /**
  5678. * hide the group, detach from DOM if needed
  5679. */
  5680. Group.prototype.hide = function hide() {
  5681. if (this.dom.label.parentNode) {
  5682. this.dom.label.parentNode.removeChild(this.dom.label);
  5683. }
  5684. if (this.itemSet) {
  5685. this.itemSet.hide();
  5686. }
  5687. var itemSetFrame = this.itemset && this.itemSet.getFrame();
  5688. if (itemSetFrame && itemSetFrame.parentNode) {
  5689. itemSetFrame.parentNode.removeChild(itemSetFrame);
  5690. }
  5691. };
  5692. /**
  5693. * Set range (start and end).
  5694. * @param {Range | Object} range A Range or an object containing start and end.
  5695. */
  5696. Group.prototype.setRange = function (range) {
  5697. this.range = range;
  5698. if (this.itemSet) this.itemSet.setRange(range);
  5699. };
  5700. /**
  5701. * Set selected items by their id. Replaces the current selection.
  5702. * Unknown id's are silently ignored.
  5703. * @param {Array} [ids] An array with zero or more id's of the items to be
  5704. * selected. If ids is an empty array, all items will be
  5705. * unselected.
  5706. */
  5707. Group.prototype.setSelection = function setSelection(ids) {
  5708. if (this.itemSet) this.itemSet.setSelection(ids);
  5709. };
  5710. /**
  5711. * Get the selected items by their id
  5712. * @return {Array} ids The ids of the selected items
  5713. */
  5714. Group.prototype.getSelection = function getSelection() {
  5715. return this.itemSet ? this.itemSet.getSelection() : [];
  5716. };
  5717. /**
  5718. * Repaint the group
  5719. * @return {boolean} Returns true if the component is resized
  5720. */
  5721. Group.prototype.repaint = function repaint() {
  5722. var resized = false;
  5723. this.show();
  5724. if (this.itemSet) {
  5725. resized = this.itemSet.repaint() || resized;
  5726. }
  5727. // calculate inner size of the label
  5728. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  5729. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  5730. this.height = this.itemSet ? this.itemSet.height : 0;
  5731. this.dom.label.style.height = this.height + 'px';
  5732. return resized;
  5733. };
  5734. /**
  5735. * An GroupSet holds a set of groups
  5736. * @param {Panel} contentPanel Panel where the ItemSets will be created
  5737. * @param {Panel} labelPanel Panel where the labels will be created
  5738. * @param {Panel} backgroundPanel Panel where the vertical lines of box
  5739. * items are created
  5740. * @param {Panel} axisPanel Panel on the axis where the dots of box
  5741. * items will be created
  5742. * @param {Object} [options] See GroupSet.setOptions for the available
  5743. * options.
  5744. * @constructor GroupSet
  5745. * @extends Panel
  5746. */
  5747. function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) {
  5748. this.id = util.randomUUID();
  5749. this.contentPanel = contentPanel;
  5750. this.labelPanel = labelPanel;
  5751. this.backgroundPanel = backgroundPanel;
  5752. this.axisPanel = axisPanel;
  5753. this.options = options || {};
  5754. this.range = null; // Range or Object {start: number, end: number}
  5755. this.itemsData = null; // DataSet with items
  5756. this.groupsData = null; // DataSet with groups
  5757. this.groups = {}; // map with groups
  5758. this.groupIds = []; // list with ordered group ids
  5759. this.dom = {};
  5760. this.props = {
  5761. labels: {
  5762. width: 0
  5763. }
  5764. };
  5765. // TODO: implement right orientation of the labels (left/right)
  5766. var me = this;
  5767. this.listeners = {
  5768. 'add': function (event, params) {
  5769. me._onAdd(params.items);
  5770. },
  5771. 'update': function (event, params) {
  5772. me._onUpdate(params.items);
  5773. },
  5774. 'remove': function (event, params) {
  5775. me._onRemove(params.items);
  5776. }
  5777. };
  5778. // create HTML DOM
  5779. this._create();
  5780. }
  5781. GroupSet.prototype = new Panel();
  5782. /**
  5783. * Create the HTML DOM elements for the GroupSet
  5784. * @private
  5785. */
  5786. GroupSet.prototype._create = function _create () {
  5787. // TODO: reimplement groupSet DOM elements
  5788. var frame = document.createElement('div');
  5789. frame.className = 'groupset';
  5790. frame['timeline-groupset'] = this;
  5791. this.frame = frame;
  5792. this.labelSet = new Panel({
  5793. className: 'labelset',
  5794. width: '100%',
  5795. height: '100%'
  5796. });
  5797. this.labelPanel.appendChild(this.labelSet);
  5798. };
  5799. /**
  5800. * Get the frame element of component
  5801. * @returns {null} Get frame is not supported by GroupSet
  5802. */
  5803. GroupSet.prototype.getFrame = function getFrame() {
  5804. return this.frame;
  5805. };
  5806. /**
  5807. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5808. * @param {Object} [options] The following options are available:
  5809. * {String | function} groupsOrder
  5810. * TODO: describe options
  5811. */
  5812. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5813. /**
  5814. * Set range (start and end).
  5815. * @param {Range | Object} range A Range or an object containing start and end.
  5816. */
  5817. GroupSet.prototype.setRange = function (range) {
  5818. this.range = range;
  5819. for (var id in this.groups) {
  5820. if (this.groups.hasOwnProperty(id)) {
  5821. this.groups[id].setRange(range);
  5822. }
  5823. }
  5824. };
  5825. /**
  5826. * Set items
  5827. * @param {vis.DataSet | null} items
  5828. */
  5829. GroupSet.prototype.setItems = function setItems(items) {
  5830. this.itemsData = items;
  5831. for (var id in this.groups) {
  5832. if (this.groups.hasOwnProperty(id)) {
  5833. var group = this.groups[id];
  5834. // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this.
  5835. group.setItems(items);
  5836. }
  5837. }
  5838. };
  5839. /**
  5840. * Get items
  5841. * @return {vis.DataSet | null} items
  5842. */
  5843. GroupSet.prototype.getItems = function getItems() {
  5844. return this.itemsData;
  5845. };
  5846. /**
  5847. * Set range (start and end).
  5848. * @param {Range | Object} range A Range or an object containing start and end.
  5849. */
  5850. GroupSet.prototype.setRange = function setRange(range) {
  5851. this.range = range;
  5852. };
  5853. /**
  5854. * Set groups
  5855. * @param {vis.DataSet} groups
  5856. */
  5857. GroupSet.prototype.setGroups = function setGroups(groups) {
  5858. var me = this,
  5859. ids;
  5860. // unsubscribe from current dataset
  5861. if (this.groupsData) {
  5862. util.forEach(this.listeners, function (callback, event) {
  5863. me.groupsData.unsubscribe(event, callback);
  5864. });
  5865. // remove all drawn groups
  5866. ids = this.groupsData.getIds();
  5867. this._onRemove(ids);
  5868. }
  5869. // replace the dataset
  5870. if (!groups) {
  5871. this.groupsData = null;
  5872. }
  5873. else if (groups instanceof DataSet) {
  5874. this.groupsData = groups;
  5875. }
  5876. else {
  5877. this.groupsData = new DataSet({
  5878. convert: {
  5879. start: 'Date',
  5880. end: 'Date'
  5881. }
  5882. });
  5883. this.groupsData.add(groups);
  5884. }
  5885. if (this.groupsData) {
  5886. // subscribe to new dataset
  5887. var id = this.id;
  5888. util.forEach(this.listeners, function (callback, event) {
  5889. me.groupsData.on(event, callback, id);
  5890. });
  5891. // draw all new groups
  5892. ids = this.groupsData.getIds();
  5893. this._onAdd(ids);
  5894. }
  5895. this.emit('change');
  5896. };
  5897. /**
  5898. * Get groups
  5899. * @return {vis.DataSet | null} groups
  5900. */
  5901. GroupSet.prototype.getGroups = function getGroups() {
  5902. return this.groupsData;
  5903. };
  5904. /**
  5905. * Set selected items by their id. Replaces the current selection.
  5906. * Unknown id's are silently ignored.
  5907. * @param {Array} [ids] An array with zero or more id's of the items to be
  5908. * selected. If ids is an empty array, all items will be
  5909. * unselected.
  5910. */
  5911. GroupSet.prototype.setSelection = function setSelection(ids) {
  5912. var selection = [],
  5913. groups = this.groups;
  5914. // iterate over each of the groups
  5915. for (var id in groups) {
  5916. if (groups.hasOwnProperty(id)) {
  5917. var group = groups[id];
  5918. group.setSelection(ids);
  5919. }
  5920. }
  5921. return selection;
  5922. };
  5923. /**
  5924. * Get the selected items by their id
  5925. * @return {Array} ids The ids of the selected items
  5926. */
  5927. GroupSet.prototype.getSelection = function getSelection() {
  5928. var selection = [],
  5929. groups = this.groups;
  5930. // iterate over each of the groups
  5931. for (var id in groups) {
  5932. if (groups.hasOwnProperty(id)) {
  5933. var group = groups[id];
  5934. selection = selection.concat(group.getSelection());
  5935. }
  5936. }
  5937. return selection;
  5938. };
  5939. /**
  5940. * Repaint the component
  5941. * @return {boolean} Returns true if the component was resized since previous repaint
  5942. */
  5943. GroupSet.prototype.repaint = function repaint() {
  5944. var i, id, group,
  5945. asSize = util.option.asSize,
  5946. asString = util.option.asString,
  5947. options = this.options,
  5948. orientation = this.getOption('orientation'),
  5949. frame = this.frame,
  5950. resized = false,
  5951. groups = this.groups;
  5952. // repaint all groups in order
  5953. this.groupIds.forEach(function (id) {
  5954. var groupResized = groups[id].repaint();
  5955. resized = resized || groupResized;
  5956. });
  5957. // reposition the labels and calculate the maximum label width
  5958. var maxWidth = 0;
  5959. for (id in groups) {
  5960. if (groups.hasOwnProperty(id)) {
  5961. group = groups[id];
  5962. maxWidth = Math.max(maxWidth, group.props.label.width);
  5963. }
  5964. }
  5965. resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized;
  5966. // recalculate the height of the groupset, and recalculate top positions of the groups
  5967. var fixedHeight = (asSize(options.height) != null);
  5968. var height;
  5969. if (!fixedHeight) {
  5970. // height is not specified, calculate the sum of the height of all groups
  5971. height = 0;
  5972. this.groupIds.forEach(function (id) {
  5973. var group = groups[id];
  5974. group.top = height;
  5975. if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack
  5976. height += group.height;
  5977. });
  5978. }
  5979. // update classname
  5980. frame.className = 'groupset' + (options.className ? (' ' + asString(options.className)) : '');
  5981. // calculate actual size and position
  5982. this.top = frame.offsetTop;
  5983. this.left = frame.offsetLeft;
  5984. this.width = frame.offsetWidth;
  5985. this.height = height;
  5986. return resized;
  5987. };
  5988. /**
  5989. * Update the groupIds. Requires a repaint afterwards
  5990. * @private
  5991. */
  5992. GroupSet.prototype._updateGroupIds = function () {
  5993. // reorder the groups
  5994. this.groupIds = this.groupsData.getIds({
  5995. order: this.options.groupOrder
  5996. });
  5997. // hide the groups now, they will be shown again in the next repaint
  5998. // in correct order
  5999. var groups = this.groups;
  6000. this.groupIds.forEach(function (id) {
  6001. groups[id].hide();
  6002. });
  6003. };
  6004. /**
  6005. * Get the width of the group labels
  6006. * @return {Number} width
  6007. */
  6008. GroupSet.prototype.getLabelsWidth = function getLabelsWidth() {
  6009. return this.props.labels.width;
  6010. };
  6011. /**
  6012. * Hide the component from the DOM
  6013. */
  6014. GroupSet.prototype.hide = function hide() {
  6015. // hide labelset
  6016. this.labelPanel.removeChild(this.labelSet);
  6017. // hide each of the groups
  6018. for (var groupId in this.groups) {
  6019. if (this.groups.hasOwnProperty(groupId)) {
  6020. this.groups[groupId].hide();
  6021. }
  6022. }
  6023. };
  6024. /**
  6025. * Show the component in the DOM (when not already visible).
  6026. * @return {Boolean} changed
  6027. */
  6028. GroupSet.prototype.show = function show() {
  6029. // show label set
  6030. if (!this.labelPanel.hasChild(this.labelSet)) {
  6031. this.labelPanel.removeChild(this.labelSet);
  6032. }
  6033. // show each of the groups
  6034. for (var groupId in this.groups) {
  6035. if (this.groups.hasOwnProperty(groupId)) {
  6036. this.groups[groupId].show();
  6037. }
  6038. }
  6039. };
  6040. /**
  6041. * Handle updated groups
  6042. * @param {Number[]} ids
  6043. * @private
  6044. */
  6045. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6046. this._onAdd(ids);
  6047. };
  6048. /**
  6049. * Handle changed groups
  6050. * @param {Number[]} ids
  6051. * @private
  6052. */
  6053. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6054. var me = this;
  6055. ids.forEach(function (id) {
  6056. var group = me.groups[id];
  6057. if (!group) {
  6058. var groupOptions = Object.create(me.options);
  6059. util.extend(groupOptions, {
  6060. height: null
  6061. });
  6062. group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions);
  6063. group.on('change', me.emit.bind(me, 'change')); // propagate change event
  6064. group.setRange(me.range);
  6065. group.setItems(me.itemsData); // attach items data
  6066. me.groups[id] = group;
  6067. group.parent = me;
  6068. }
  6069. // update group data
  6070. group.setData(me.groupsData.get(id));
  6071. });
  6072. this._updateGroupIds();
  6073. this.emit('change');
  6074. };
  6075. /**
  6076. * Handle removed groups
  6077. * @param {Number[]} ids
  6078. * @private
  6079. */
  6080. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6081. var groups = this.groups;
  6082. ids.forEach(function (id) {
  6083. var group = groups[id];
  6084. if (group) {
  6085. group.setItems(); // detach items data
  6086. group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
  6087. delete groups[id];
  6088. }
  6089. });
  6090. this._updateGroupIds();
  6091. this.emit('change');
  6092. };
  6093. /**
  6094. * Find the GroupSet from an event target:
  6095. * searches for the attribute 'timeline-groupset' in the event target's element
  6096. * tree, then finds the right group in this groupset
  6097. * @param {Event} event
  6098. * @return {Group | null} group
  6099. */
  6100. GroupSet.groupSetFromTarget = function groupSetFromTarget (event) {
  6101. var target = event.target;
  6102. while (target) {
  6103. if (target.hasOwnProperty('timeline-groupset')) {
  6104. return target['timeline-groupset'];
  6105. }
  6106. target = target.parentNode;
  6107. }
  6108. return null;
  6109. };
  6110. /**
  6111. * Find the Group from an event target:
  6112. * searches for the two elements having attributes 'timeline-groupset' and
  6113. * 'timeline-itemset' in the event target's element, then finds the right group.
  6114. * @param {Event} event
  6115. * @return {Group | null} group
  6116. */
  6117. GroupSet.groupFromTarget = function groupFromTarget (event) {
  6118. // find the groupSet
  6119. var groupSet = GroupSet.groupSetFromTarget(event);
  6120. // find the ItemSet
  6121. var itemSet = ItemSet.itemSetFromTarget(event);
  6122. // find the right group
  6123. if (groupSet && itemSet) {
  6124. for (var groupId in groupSet.groups) {
  6125. if (groupSet.groups.hasOwnProperty(groupId)) {
  6126. var group = groupSet.groups[groupId];
  6127. if (group.itemSet == itemSet) {
  6128. return group;
  6129. }
  6130. }
  6131. }
  6132. }
  6133. return null;
  6134. };
  6135. /**
  6136. * Create a timeline visualization
  6137. * @param {HTMLElement} container
  6138. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  6139. * @param {Object} [options] See Timeline.setOptions for the available options.
  6140. * @constructor
  6141. */
  6142. function Timeline (container, items, options) {
  6143. // validate arguments
  6144. if (!container) throw new Error('No container element provided');
  6145. var me = this;
  6146. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6147. this.options = {
  6148. orientation: 'bottom',
  6149. direction: 'horizontal', // 'horizontal' or 'vertical'
  6150. autoResize: true,
  6151. editable: false,
  6152. selectable: true,
  6153. snap: null, // will be specified after timeaxis is created
  6154. min: null,
  6155. max: null,
  6156. zoomMin: 10, // milliseconds
  6157. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6158. // moveable: true, // TODO: option moveable
  6159. // zoomable: true, // TODO: option zoomable
  6160. showMinorLabels: true,
  6161. showMajorLabels: true,
  6162. showCurrentTime: false,
  6163. showCustomTime: false,
  6164. type: 'box',
  6165. align: 'center',
  6166. margin: {
  6167. axis: 20,
  6168. item: 10
  6169. },
  6170. padding: 5,
  6171. onAdd: function (item, callback) {
  6172. callback(item);
  6173. },
  6174. onUpdate: function (item, callback) {
  6175. callback(item);
  6176. },
  6177. onMove: function (item, callback) {
  6178. callback(item);
  6179. },
  6180. onRemove: function (item, callback) {
  6181. callback(item);
  6182. },
  6183. toScreen: me._toScreen.bind(me),
  6184. toTime: me._toTime.bind(me)
  6185. };
  6186. // root panel
  6187. var rootOptions = util.extend(Object.create(this.options), {
  6188. height: function () {
  6189. if (me.options.height) {
  6190. // fixed height
  6191. return me.options.height;
  6192. }
  6193. else {
  6194. // auto height
  6195. // TODO: implement a css based solution to automatically have the right hight
  6196. return (me.timeAxis.height + me.contentPanel.height) + 'px';
  6197. }
  6198. }
  6199. });
  6200. this.rootPanel = new RootPanel(container, rootOptions);
  6201. // single select (or unselect) when tapping an item
  6202. this.rootPanel.on('tap', this._onSelectItem.bind(this));
  6203. // multi select when holding mouse/touch, or on ctrl+click
  6204. this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
  6205. // add item on doubletap
  6206. this.rootPanel.on('doubletap', this._onAddItem.bind(this));
  6207. // side panel
  6208. var sideOptions = util.extend(Object.create(this.options), {
  6209. top: function () {
  6210. return (sideOptions.orientation == 'top') ? '0' : '';
  6211. },
  6212. bottom: function () {
  6213. return (sideOptions.orientation == 'top') ? '' : '0';
  6214. },
  6215. left: '0',
  6216. right: null,
  6217. height: '100%',
  6218. width: function () {
  6219. if (me.groupSet) {
  6220. return me.groupSet.getLabelsWidth();
  6221. }
  6222. else {
  6223. return 0;
  6224. }
  6225. },
  6226. className: function () {
  6227. return 'side' + (me.groupsData ? '' : ' hidden');
  6228. }
  6229. });
  6230. this.sidePanel = new Panel(sideOptions);
  6231. this.rootPanel.appendChild(this.sidePanel);
  6232. // main panel (contains time axis and itemsets)
  6233. var mainOptions = util.extend(Object.create(this.options), {
  6234. left: function () {
  6235. // we align left to enable a smooth resizing of the window
  6236. return me.sidePanel.width;
  6237. },
  6238. right: null,
  6239. height: '100%',
  6240. width: function () {
  6241. return me.rootPanel.width - me.sidePanel.width;
  6242. },
  6243. className: 'main'
  6244. });
  6245. this.mainPanel = new Panel(mainOptions);
  6246. this.rootPanel.appendChild(this.mainPanel);
  6247. // range
  6248. // TODO: move range inside rootPanel?
  6249. var rangeOptions = Object.create(this.options);
  6250. this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
  6251. this.range.setRange(
  6252. now.clone().add('days', -3).valueOf(),
  6253. now.clone().add('days', 4).valueOf()
  6254. );
  6255. this.range.on('rangechange', function (properties) {
  6256. me.rootPanel.repaint();
  6257. me.emit('rangechange', properties);
  6258. });
  6259. this.range.on('rangechanged', function (properties) {
  6260. me.rootPanel.repaint();
  6261. me.emit('rangechanged', properties);
  6262. });
  6263. // panel with time axis
  6264. var timeAxisOptions = util.extend(Object.create(rootOptions), {
  6265. range: this.range,
  6266. left: null,
  6267. top: null,
  6268. width: null,
  6269. height: null
  6270. });
  6271. this.timeAxis = new TimeAxis(timeAxisOptions);
  6272. this.timeAxis.setRange(this.range);
  6273. this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
  6274. this.mainPanel.appendChild(this.timeAxis);
  6275. // content panel (contains itemset(s))
  6276. var contentOptions = util.extend(Object.create(this.options), {
  6277. top: function () {
  6278. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6279. },
  6280. bottom: function () {
  6281. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6282. },
  6283. left: null,
  6284. right: null,
  6285. height: null,
  6286. width: null,
  6287. className: 'content'
  6288. });
  6289. this.contentPanel = new Panel(contentOptions);
  6290. this.mainPanel.appendChild(this.contentPanel);
  6291. // content panel (contains the vertical lines of box items)
  6292. var backgroundOptions = util.extend(Object.create(this.options), {
  6293. top: function () {
  6294. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6295. },
  6296. bottom: function () {
  6297. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6298. },
  6299. left: null,
  6300. right: null,
  6301. height: function () {
  6302. return me.contentPanel.height;
  6303. },
  6304. width: null,
  6305. className: 'background'
  6306. });
  6307. this.backgroundPanel = new Panel(backgroundOptions);
  6308. this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
  6309. // panel with axis holding the dots of item boxes
  6310. var axisPanelOptions = util.extend(Object.create(rootOptions), {
  6311. left: 0,
  6312. top: function () {
  6313. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6314. },
  6315. bottom: function () {
  6316. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6317. },
  6318. width: '100%',
  6319. height: 0,
  6320. className: 'axis'
  6321. });
  6322. this.axisPanel = new Panel(axisPanelOptions);
  6323. this.mainPanel.appendChild(this.axisPanel);
  6324. // content panel (contains itemset(s))
  6325. var sideContentOptions = util.extend(Object.create(this.options), {
  6326. top: function () {
  6327. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6328. },
  6329. bottom: function () {
  6330. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6331. },
  6332. left: null,
  6333. right: null,
  6334. height: null,
  6335. width: null,
  6336. className: 'side-content'
  6337. });
  6338. this.sideContentPanel = new Panel(sideContentOptions);
  6339. this.sidePanel.appendChild(this.sideContentPanel);
  6340. // current time bar
  6341. // Note: time bar will be attached in this.setOptions when selected
  6342. this.currentTime = new CurrentTime(this.range, rootOptions);
  6343. // custom time bar
  6344. // Note: time bar will be attached in this.setOptions when selected
  6345. this.customTime = new CustomTime(rootOptions);
  6346. this.customTime.on('timechange', function (time) {
  6347. me.emit('timechange', time);
  6348. });
  6349. this.customTime.on('timechanged', function (time) {
  6350. me.emit('timechanged', time);
  6351. });
  6352. this.itemSet = null;
  6353. this.groupSet = null;
  6354. // create groupset
  6355. this.setGroups(null);
  6356. this.itemsData = null; // DataSet
  6357. this.groupsData = null; // DataSet
  6358. // apply options
  6359. if (options) {
  6360. this.setOptions(options);
  6361. }
  6362. // create itemset and groupset
  6363. if (items) {
  6364. this.setItems(items);
  6365. }
  6366. }
  6367. // turn Timeline into an event emitter
  6368. Emitter(Timeline.prototype);
  6369. /**
  6370. * Set options
  6371. * @param {Object} options TODO: describe the available options
  6372. */
  6373. Timeline.prototype.setOptions = function (options) {
  6374. util.extend(this.options, options);
  6375. // force update of range (apply new min/max etc.)
  6376. // both start and end are optional
  6377. this.range.setRange(options.start, options.end);
  6378. if ('editable' in options || 'selectable' in options) {
  6379. if (this.options.selectable) {
  6380. // force update of selection
  6381. this.setSelection(this.getSelection());
  6382. }
  6383. else {
  6384. // remove selection
  6385. this.setSelection([]);
  6386. }
  6387. }
  6388. // validate the callback functions
  6389. var validateCallback = (function (fn) {
  6390. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  6391. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  6392. }
  6393. }).bind(this);
  6394. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  6395. // add/remove the current time bar
  6396. if (this.options.showCurrentTime) {
  6397. if (!this.mainPanel.hasChild(this.currentTime)) {
  6398. this.mainPanel.appendChild(this.currentTime);
  6399. this.currentTime.start();
  6400. }
  6401. }
  6402. else {
  6403. if (this.mainPanel.hasChild(this.currentTime)) {
  6404. this.currentTime.stop();
  6405. this.mainPanel.removeChild(this.currentTime);
  6406. }
  6407. }
  6408. // add/remove the custom time bar
  6409. if (this.options.showCustomTime) {
  6410. if (!this.mainPanel.hasChild(this.customTime)) {
  6411. this.mainPanel.appendChild(this.customTime);
  6412. }
  6413. }
  6414. else {
  6415. if (this.mainPanel.hasChild(this.customTime)) {
  6416. this.mainPanel.removeChild(this.customTime);
  6417. }
  6418. }
  6419. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  6420. if (options && options.order) {
  6421. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  6422. }
  6423. // repaint everything
  6424. this.rootPanel.repaint();
  6425. };
  6426. /**
  6427. * Set a custom time bar
  6428. * @param {Date} time
  6429. */
  6430. Timeline.prototype.setCustomTime = function (time) {
  6431. if (!this.customTime) {
  6432. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6433. }
  6434. this.customTime.setCustomTime(time);
  6435. };
  6436. /**
  6437. * Retrieve the current custom time.
  6438. * @return {Date} customTime
  6439. */
  6440. Timeline.prototype.getCustomTime = function() {
  6441. if (!this.customTime) {
  6442. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6443. }
  6444. return this.customTime.getCustomTime();
  6445. };
  6446. /**
  6447. * Set items
  6448. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  6449. */
  6450. Timeline.prototype.setItems = function(items) {
  6451. var initialLoad = (this.itemsData == null);
  6452. // convert to type DataSet when needed
  6453. var newDataSet;
  6454. if (!items) {
  6455. newDataSet = null;
  6456. }
  6457. else if (items instanceof DataSet) {
  6458. newDataSet = items;
  6459. }
  6460. if (!(items instanceof DataSet)) {
  6461. newDataSet = new DataSet({
  6462. convert: {
  6463. start: 'Date',
  6464. end: 'Date'
  6465. }
  6466. });
  6467. newDataSet.add(items);
  6468. }
  6469. // set items
  6470. this.itemsData = newDataSet;
  6471. (this.itemSet || this.groupSet).setItems(newDataSet);
  6472. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6473. // apply the data range as range
  6474. var dataRange = this.getItemRange();
  6475. // add 5% space on both sides
  6476. var start = dataRange.min;
  6477. var end = dataRange.max;
  6478. if (start != null && end != null) {
  6479. var interval = (end.valueOf() - start.valueOf());
  6480. if (interval <= 0) {
  6481. // prevent an empty interval
  6482. interval = 24 * 60 * 60 * 1000; // 1 day
  6483. }
  6484. start = new Date(start.valueOf() - interval * 0.05);
  6485. end = new Date(end.valueOf() + interval * 0.05);
  6486. }
  6487. // override specified start and/or end date
  6488. if (this.options.start != undefined) {
  6489. start = util.convert(this.options.start, 'Date');
  6490. }
  6491. if (this.options.end != undefined) {
  6492. end = util.convert(this.options.end, 'Date');
  6493. }
  6494. // skip range set if there is no start and end date
  6495. if (start === null && end === null) {
  6496. return;
  6497. }
  6498. // if start and end dates are set but cannot be satisfyed due to zoom restrictions — correct end date
  6499. if (start != null && end != null) {
  6500. var diff = end.valueOf() - start.valueOf();
  6501. if (this.options.zoomMax != undefined && this.options.zoomMax < diff) {
  6502. end = new Date(start.valueOf() + this.options.zoomMax);
  6503. }
  6504. if (this.options.zoomMin != undefined && this.options.zoomMin > diff) {
  6505. end = new Date(start.valueOf() + this.options.zoomMin);
  6506. }
  6507. }
  6508. this.range.setRange(start, end);
  6509. }
  6510. };
  6511. /**
  6512. * Set groups
  6513. * @param {vis.DataSet | Array | google.visualization.DataTable} groupSet
  6514. */
  6515. Timeline.prototype.setGroups = function(groupSet) {
  6516. var me = this;
  6517. this.groupsData = groupSet;
  6518. // create options for the itemset or groupset
  6519. var options = util.extend(Object.create(this.options), {
  6520. top: null,
  6521. bottom: null,
  6522. right: null,
  6523. left: null,
  6524. width: null,
  6525. height: null
  6526. });
  6527. if (this.groupsData) {
  6528. // Create a GroupSet
  6529. // remove itemset if existing
  6530. if (this.itemSet) {
  6531. this.itemSet.hide(); // TODO: not so nice having to hide here
  6532. this.contentPanel.removeChild(this.itemSet);
  6533. this.itemSet.setItems(); // disconnect from itemset
  6534. this.itemSet = null;
  6535. }
  6536. // create new GroupSet when needed
  6537. if (!this.groupSet) {
  6538. this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, this.backgroundPanel, this.axisPanel, options);
  6539. this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel));
  6540. this.groupSet.setRange(this.range);
  6541. this.groupSet.setItems(this.itemsData);
  6542. this.groupSet.setGroups(this.groupsData);
  6543. this.contentPanel.appendChild(this.groupSet);
  6544. }
  6545. else {
  6546. this.groupSet.setGroups(this.groupsData);
  6547. }
  6548. }
  6549. else {
  6550. // ItemSet
  6551. if (this.groupSet) {
  6552. this.groupSet.hide(); // TODO: not so nice having to hide here
  6553. //this.groupSet.setGroups(); // disconnect from groupset
  6554. this.groupSet.setItems(); // disconnect from itemset
  6555. this.contentPanel.removeChild(this.groupSet);
  6556. this.groupSet = null;
  6557. }
  6558. // create new items
  6559. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, options);
  6560. this.itemSet.setRange(this.range);
  6561. this.itemSet.setItems(this.itemsData);
  6562. this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
  6563. this.contentPanel.appendChild(this.itemSet);
  6564. }
  6565. };
  6566. /**
  6567. * Get the data range of the item set.
  6568. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6569. * When no minimum is found, min==null
  6570. * When no maximum is found, max==null
  6571. */
  6572. Timeline.prototype.getItemRange = function getItemRange() {
  6573. // calculate min from start filed
  6574. var itemsData = this.itemsData,
  6575. min = null,
  6576. max = null;
  6577. if (itemsData) {
  6578. // calculate the minimum value of the field 'start'
  6579. var minItem = itemsData.min('start');
  6580. min = minItem ? minItem.start.valueOf() : null;
  6581. // calculate maximum value of fields 'start' and 'end'
  6582. var maxStartItem = itemsData.max('start');
  6583. if (maxStartItem) {
  6584. max = maxStartItem.start.valueOf();
  6585. }
  6586. var maxEndItem = itemsData.max('end');
  6587. if (maxEndItem) {
  6588. if (max == null) {
  6589. max = maxEndItem.end.valueOf();
  6590. }
  6591. else {
  6592. max = Math.max(max, maxEndItem.end.valueOf());
  6593. }
  6594. }
  6595. }
  6596. return {
  6597. min: (min != null) ? new Date(min) : null,
  6598. max: (max != null) ? new Date(max) : null
  6599. };
  6600. };
  6601. /**
  6602. * Set selected items by their id. Replaces the current selection
  6603. * Unknown id's are silently ignored.
  6604. * @param {Array} [ids] An array with zero or more id's of the items to be
  6605. * selected. If ids is an empty array, all items will be
  6606. * unselected.
  6607. */
  6608. Timeline.prototype.setSelection = function setSelection (ids) {
  6609. var itemOrGroupSet = (this.itemSet || this.groupSet);
  6610. if (itemOrGroupSet) itemOrGroupSet.setSelection(ids);
  6611. };
  6612. /**
  6613. * Get the selected items by their id
  6614. * @return {Array} ids The ids of the selected items
  6615. */
  6616. Timeline.prototype.getSelection = function getSelection() {
  6617. var itemOrGroupSet = (this.itemSet || this.groupSet);
  6618. return itemOrGroupSet ? itemOrGroupSet.getSelection() : [];
  6619. };
  6620. /**
  6621. * Set the visible window. Both parameters are optional, you can change only
  6622. * start or only end. Syntax:
  6623. *
  6624. * TimeLine.setWindow(start, end)
  6625. * TimeLine.setWindow(range)
  6626. *
  6627. * Where start and end can be a Date, number, or string, and range is an
  6628. * object with properties start and end.
  6629. *
  6630. * @param {Date | Number | String} [start] Start date of visible window
  6631. * @param {Date | Number | String} [end] End date of visible window
  6632. */
  6633. Timeline.prototype.setWindow = function setWindow(start, end) {
  6634. if (arguments.length == 1) {
  6635. var range = arguments[0];
  6636. this.range.setRange(range.start, range.end);
  6637. }
  6638. else {
  6639. this.range.setRange(start, end);
  6640. }
  6641. };
  6642. /**
  6643. * Get the visible window
  6644. * @return {{start: Date, end: Date}} Visible range
  6645. */
  6646. Timeline.prototype.getWindow = function setWindow() {
  6647. var range = this.range.getRange();
  6648. return {
  6649. start: new Date(range.start),
  6650. end: new Date(range.end)
  6651. };
  6652. };
  6653. /**
  6654. * Handle selecting/deselecting an item when tapping it
  6655. * @param {Event} event
  6656. * @private
  6657. */
  6658. // TODO: move this function to ItemSet
  6659. Timeline.prototype._onSelectItem = function (event) {
  6660. if (!this.options.selectable) return;
  6661. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  6662. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  6663. if (ctrlKey || shiftKey) {
  6664. this._onMultiSelectItem(event);
  6665. return;
  6666. }
  6667. var oldSelection = this.getSelection();
  6668. var item = ItemSet.itemFromTarget(event);
  6669. var selection = item ? [item.id] : [];
  6670. this.setSelection(selection);
  6671. var newSelection = this.getSelection();
  6672. // if selection is changed, emit a select event
  6673. if (!util.equalArray(oldSelection, newSelection)) {
  6674. this.emit('select', {
  6675. items: this.getSelection()
  6676. });
  6677. }
  6678. event.stopPropagation();
  6679. };
  6680. /**
  6681. * Handle creation and updates of an item on double tap
  6682. * @param event
  6683. * @private
  6684. */
  6685. Timeline.prototype._onAddItem = function (event) {
  6686. if (!this.options.selectable) return;
  6687. if (!this.options.editable) return;
  6688. var me = this,
  6689. item = ItemSet.itemFromTarget(event);
  6690. if (item) {
  6691. // update item
  6692. // execute async handler to update the item (or cancel it)
  6693. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  6694. this.options.onUpdate(itemData, function (itemData) {
  6695. if (itemData) {
  6696. me.itemsData.update(itemData);
  6697. }
  6698. });
  6699. }
  6700. else {
  6701. // add item
  6702. var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
  6703. var x = event.gesture.center.pageX - xAbs;
  6704. var newItem = {
  6705. start: this.timeAxis.snap(this._toTime(x)),
  6706. content: 'new item'
  6707. };
  6708. var id = util.randomUUID();
  6709. newItem[this.itemsData.fieldId] = id;
  6710. var group = GroupSet.groupFromTarget(event);
  6711. if (group) {
  6712. newItem.group = group.groupId;
  6713. }
  6714. // execute async handler to customize (or cancel) adding an item
  6715. this.options.onAdd(newItem, function (item) {
  6716. if (item) {
  6717. me.itemsData.add(newItem);
  6718. // TODO: need to trigger a repaint?
  6719. }
  6720. });
  6721. }
  6722. };
  6723. /**
  6724. * Handle selecting/deselecting multiple items when holding an item
  6725. * @param {Event} event
  6726. * @private
  6727. */
  6728. // TODO: move this function to ItemSet
  6729. Timeline.prototype._onMultiSelectItem = function (event) {
  6730. if (!this.options.selectable) return;
  6731. var selection,
  6732. item = ItemSet.itemFromTarget(event);
  6733. if (item) {
  6734. // multi select items
  6735. selection = this.getSelection(); // current selection
  6736. var index = selection.indexOf(item.id);
  6737. if (index == -1) {
  6738. // item is not yet selected -> select it
  6739. selection.push(item.id);
  6740. }
  6741. else {
  6742. // item is already selected -> deselect it
  6743. selection.splice(index, 1);
  6744. }
  6745. this.setSelection(selection);
  6746. this.emit('select', {
  6747. items: this.getSelection()
  6748. });
  6749. event.stopPropagation();
  6750. }
  6751. };
  6752. /**
  6753. * Convert a position on screen (pixels) to a datetime
  6754. * @param {int} x Position on the screen in pixels
  6755. * @return {Date} time The datetime the corresponds with given position x
  6756. * @private
  6757. */
  6758. Timeline.prototype._toTime = function _toTime(x) {
  6759. var conversion = this.range.conversion(this.mainPanel.width);
  6760. return new Date(x / conversion.scale + conversion.offset);
  6761. };
  6762. /**
  6763. * Convert a datetime (Date object) into a position on the screen
  6764. * @param {Date} time A date
  6765. * @return {int} x The position on the screen in pixels which corresponds
  6766. * with the given date.
  6767. * @private
  6768. */
  6769. Timeline.prototype._toScreen = function _toScreen(time) {
  6770. var conversion = this.range.conversion(this.mainPanel.width);
  6771. return (time.valueOf() - conversion.offset) * conversion.scale;
  6772. };
  6773. (function(exports) {
  6774. /**
  6775. * Parse a text source containing data in DOT language into a JSON object.
  6776. * The object contains two lists: one with nodes and one with edges.
  6777. *
  6778. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6779. *
  6780. * @param {String} data Text containing a graph in DOT-notation
  6781. * @return {Object} graph An object containing two parameters:
  6782. * {Object[]} nodes
  6783. * {Object[]} edges
  6784. */
  6785. function parseDOT (data) {
  6786. dot = data;
  6787. return parseGraph();
  6788. }
  6789. // token types enumeration
  6790. var TOKENTYPE = {
  6791. NULL : 0,
  6792. DELIMITER : 1,
  6793. IDENTIFIER: 2,
  6794. UNKNOWN : 3
  6795. };
  6796. // map with all delimiters
  6797. var DELIMITERS = {
  6798. '{': true,
  6799. '}': true,
  6800. '[': true,
  6801. ']': true,
  6802. ';': true,
  6803. '=': true,
  6804. ',': true,
  6805. '->': true,
  6806. '--': true
  6807. };
  6808. var dot = ''; // current dot file
  6809. var index = 0; // current index in dot file
  6810. var c = ''; // current token character in expr
  6811. var token = ''; // current token
  6812. var tokenType = TOKENTYPE.NULL; // type of the token
  6813. /**
  6814. * Get the first character from the dot file.
  6815. * The character is stored into the char c. If the end of the dot file is
  6816. * reached, the function puts an empty string in c.
  6817. */
  6818. function first() {
  6819. index = 0;
  6820. c = dot.charAt(0);
  6821. }
  6822. /**
  6823. * Get the next character from the dot file.
  6824. * The character is stored into the char c. If the end of the dot file is
  6825. * reached, the function puts an empty string in c.
  6826. */
  6827. function next() {
  6828. index++;
  6829. c = dot.charAt(index);
  6830. }
  6831. /**
  6832. * Preview the next character from the dot file.
  6833. * @return {String} cNext
  6834. */
  6835. function nextPreview() {
  6836. return dot.charAt(index + 1);
  6837. }
  6838. /**
  6839. * Test whether given character is alphabetic or numeric
  6840. * @param {String} c
  6841. * @return {Boolean} isAlphaNumeric
  6842. */
  6843. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6844. function isAlphaNumeric(c) {
  6845. return regexAlphaNumeric.test(c);
  6846. }
  6847. /**
  6848. * Merge all properties of object b into object b
  6849. * @param {Object} a
  6850. * @param {Object} b
  6851. * @return {Object} a
  6852. */
  6853. function merge (a, b) {
  6854. if (!a) {
  6855. a = {};
  6856. }
  6857. if (b) {
  6858. for (var name in b) {
  6859. if (b.hasOwnProperty(name)) {
  6860. a[name] = b[name];
  6861. }
  6862. }
  6863. }
  6864. return a;
  6865. }
  6866. /**
  6867. * Set a value in an object, where the provided parameter name can be a
  6868. * path with nested parameters. For example:
  6869. *
  6870. * var obj = {a: 2};
  6871. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6872. *
  6873. * @param {Object} obj
  6874. * @param {String} path A parameter name or dot-separated parameter path,
  6875. * like "color.highlight.border".
  6876. * @param {*} value
  6877. */
  6878. function setValue(obj, path, value) {
  6879. var keys = path.split('.');
  6880. var o = obj;
  6881. while (keys.length) {
  6882. var key = keys.shift();
  6883. if (keys.length) {
  6884. // this isn't the end point
  6885. if (!o[key]) {
  6886. o[key] = {};
  6887. }
  6888. o = o[key];
  6889. }
  6890. else {
  6891. // this is the end point
  6892. o[key] = value;
  6893. }
  6894. }
  6895. }
  6896. /**
  6897. * Add a node to a graph object. If there is already a node with
  6898. * the same id, their attributes will be merged.
  6899. * @param {Object} graph
  6900. * @param {Object} node
  6901. */
  6902. function addNode(graph, node) {
  6903. var i, len;
  6904. var current = null;
  6905. // find root graph (in case of subgraph)
  6906. var graphs = [graph]; // list with all graphs from current graph to root graph
  6907. var root = graph;
  6908. while (root.parent) {
  6909. graphs.push(root.parent);
  6910. root = root.parent;
  6911. }
  6912. // find existing node (at root level) by its id
  6913. if (root.nodes) {
  6914. for (i = 0, len = root.nodes.length; i < len; i++) {
  6915. if (node.id === root.nodes[i].id) {
  6916. current = root.nodes[i];
  6917. break;
  6918. }
  6919. }
  6920. }
  6921. if (!current) {
  6922. // this is a new node
  6923. current = {
  6924. id: node.id
  6925. };
  6926. if (graph.node) {
  6927. // clone default attributes
  6928. current.attr = merge(current.attr, graph.node);
  6929. }
  6930. }
  6931. // add node to this (sub)graph and all its parent graphs
  6932. for (i = graphs.length - 1; i >= 0; i--) {
  6933. var g = graphs[i];
  6934. if (!g.nodes) {
  6935. g.nodes = [];
  6936. }
  6937. if (g.nodes.indexOf(current) == -1) {
  6938. g.nodes.push(current);
  6939. }
  6940. }
  6941. // merge attributes
  6942. if (node.attr) {
  6943. current.attr = merge(current.attr, node.attr);
  6944. }
  6945. }
  6946. /**
  6947. * Add an edge to a graph object
  6948. * @param {Object} graph
  6949. * @param {Object} edge
  6950. */
  6951. function addEdge(graph, edge) {
  6952. if (!graph.edges) {
  6953. graph.edges = [];
  6954. }
  6955. graph.edges.push(edge);
  6956. if (graph.edge) {
  6957. var attr = merge({}, graph.edge); // clone default attributes
  6958. edge.attr = merge(attr, edge.attr); // merge attributes
  6959. }
  6960. }
  6961. /**
  6962. * Create an edge to a graph object
  6963. * @param {Object} graph
  6964. * @param {String | Number | Object} from
  6965. * @param {String | Number | Object} to
  6966. * @param {String} type
  6967. * @param {Object | null} attr
  6968. * @return {Object} edge
  6969. */
  6970. function createEdge(graph, from, to, type, attr) {
  6971. var edge = {
  6972. from: from,
  6973. to: to,
  6974. type: type
  6975. };
  6976. if (graph.edge) {
  6977. edge.attr = merge({}, graph.edge); // clone default attributes
  6978. }
  6979. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  6980. return edge;
  6981. }
  6982. /**
  6983. * Get next token in the current dot file.
  6984. * The token and token type are available as token and tokenType
  6985. */
  6986. function getToken() {
  6987. tokenType = TOKENTYPE.NULL;
  6988. token = '';
  6989. // skip over whitespaces
  6990. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6991. next();
  6992. }
  6993. do {
  6994. var isComment = false;
  6995. // skip comment
  6996. if (c == '#') {
  6997. // find the previous non-space character
  6998. var i = index - 1;
  6999. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7000. i--;
  7001. }
  7002. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7003. // the # is at the start of a line, this is indeed a line comment
  7004. while (c != '' && c != '\n') {
  7005. next();
  7006. }
  7007. isComment = true;
  7008. }
  7009. }
  7010. if (c == '/' && nextPreview() == '/') {
  7011. // skip line comment
  7012. while (c != '' && c != '\n') {
  7013. next();
  7014. }
  7015. isComment = true;
  7016. }
  7017. if (c == '/' && nextPreview() == '*') {
  7018. // skip block comment
  7019. while (c != '') {
  7020. if (c == '*' && nextPreview() == '/') {
  7021. // end of block comment found. skip these last two characters
  7022. next();
  7023. next();
  7024. break;
  7025. }
  7026. else {
  7027. next();
  7028. }
  7029. }
  7030. isComment = true;
  7031. }
  7032. // skip over whitespaces
  7033. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7034. next();
  7035. }
  7036. }
  7037. while (isComment);
  7038. // check for end of dot file
  7039. if (c == '') {
  7040. // token is still empty
  7041. tokenType = TOKENTYPE.DELIMITER;
  7042. return;
  7043. }
  7044. // check for delimiters consisting of 2 characters
  7045. var c2 = c + nextPreview();
  7046. if (DELIMITERS[c2]) {
  7047. tokenType = TOKENTYPE.DELIMITER;
  7048. token = c2;
  7049. next();
  7050. next();
  7051. return;
  7052. }
  7053. // check for delimiters consisting of 1 character
  7054. if (DELIMITERS[c]) {
  7055. tokenType = TOKENTYPE.DELIMITER;
  7056. token = c;
  7057. next();
  7058. return;
  7059. }
  7060. // check for an identifier (number or string)
  7061. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7062. if (isAlphaNumeric(c) || c == '-') {
  7063. token += c;
  7064. next();
  7065. while (isAlphaNumeric(c)) {
  7066. token += c;
  7067. next();
  7068. }
  7069. if (token == 'false') {
  7070. token = false; // convert to boolean
  7071. }
  7072. else if (token == 'true') {
  7073. token = true; // convert to boolean
  7074. }
  7075. else if (!isNaN(Number(token))) {
  7076. token = Number(token); // convert to number
  7077. }
  7078. tokenType = TOKENTYPE.IDENTIFIER;
  7079. return;
  7080. }
  7081. // check for a string enclosed by double quotes
  7082. if (c == '"') {
  7083. next();
  7084. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7085. token += c;
  7086. if (c == '"') { // skip the escape character
  7087. next();
  7088. }
  7089. next();
  7090. }
  7091. if (c != '"') {
  7092. throw newSyntaxError('End of string " expected');
  7093. }
  7094. next();
  7095. tokenType = TOKENTYPE.IDENTIFIER;
  7096. return;
  7097. }
  7098. // something unknown is found, wrong characters, a syntax error
  7099. tokenType = TOKENTYPE.UNKNOWN;
  7100. while (c != '') {
  7101. token += c;
  7102. next();
  7103. }
  7104. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7105. }
  7106. /**
  7107. * Parse a graph.
  7108. * @returns {Object} graph
  7109. */
  7110. function parseGraph() {
  7111. var graph = {};
  7112. first();
  7113. getToken();
  7114. // optional strict keyword
  7115. if (token == 'strict') {
  7116. graph.strict = true;
  7117. getToken();
  7118. }
  7119. // graph or digraph keyword
  7120. if (token == 'graph' || token == 'digraph') {
  7121. graph.type = token;
  7122. getToken();
  7123. }
  7124. // optional graph id
  7125. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7126. graph.id = token;
  7127. getToken();
  7128. }
  7129. // open angle bracket
  7130. if (token != '{') {
  7131. throw newSyntaxError('Angle bracket { expected');
  7132. }
  7133. getToken();
  7134. // statements
  7135. parseStatements(graph);
  7136. // close angle bracket
  7137. if (token != '}') {
  7138. throw newSyntaxError('Angle bracket } expected');
  7139. }
  7140. getToken();
  7141. // end of file
  7142. if (token !== '') {
  7143. throw newSyntaxError('End of file expected');
  7144. }
  7145. getToken();
  7146. // remove temporary default properties
  7147. delete graph.node;
  7148. delete graph.edge;
  7149. delete graph.graph;
  7150. return graph;
  7151. }
  7152. /**
  7153. * Parse a list with statements.
  7154. * @param {Object} graph
  7155. */
  7156. function parseStatements (graph) {
  7157. while (token !== '' && token != '}') {
  7158. parseStatement(graph);
  7159. if (token == ';') {
  7160. getToken();
  7161. }
  7162. }
  7163. }
  7164. /**
  7165. * Parse a single statement. Can be a an attribute statement, node
  7166. * statement, a series of node statements and edge statements, or a
  7167. * parameter.
  7168. * @param {Object} graph
  7169. */
  7170. function parseStatement(graph) {
  7171. // parse subgraph
  7172. var subgraph = parseSubgraph(graph);
  7173. if (subgraph) {
  7174. // edge statements
  7175. parseEdge(graph, subgraph);
  7176. return;
  7177. }
  7178. // parse an attribute statement
  7179. var attr = parseAttributeStatement(graph);
  7180. if (attr) {
  7181. return;
  7182. }
  7183. // parse node
  7184. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7185. throw newSyntaxError('Identifier expected');
  7186. }
  7187. var id = token; // id can be a string or a number
  7188. getToken();
  7189. if (token == '=') {
  7190. // id statement
  7191. getToken();
  7192. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7193. throw newSyntaxError('Identifier expected');
  7194. }
  7195. graph[id] = token;
  7196. getToken();
  7197. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7198. }
  7199. else {
  7200. parseNodeStatement(graph, id);
  7201. }
  7202. }
  7203. /**
  7204. * Parse a subgraph
  7205. * @param {Object} graph parent graph object
  7206. * @return {Object | null} subgraph
  7207. */
  7208. function parseSubgraph (graph) {
  7209. var subgraph = null;
  7210. // optional subgraph keyword
  7211. if (token == 'subgraph') {
  7212. subgraph = {};
  7213. subgraph.type = 'subgraph';
  7214. getToken();
  7215. // optional graph id
  7216. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7217. subgraph.id = token;
  7218. getToken();
  7219. }
  7220. }
  7221. // open angle bracket
  7222. if (token == '{') {
  7223. getToken();
  7224. if (!subgraph) {
  7225. subgraph = {};
  7226. }
  7227. subgraph.parent = graph;
  7228. subgraph.node = graph.node;
  7229. subgraph.edge = graph.edge;
  7230. subgraph.graph = graph.graph;
  7231. // statements
  7232. parseStatements(subgraph);
  7233. // close angle bracket
  7234. if (token != '}') {
  7235. throw newSyntaxError('Angle bracket } expected');
  7236. }
  7237. getToken();
  7238. // remove temporary default properties
  7239. delete subgraph.node;
  7240. delete subgraph.edge;
  7241. delete subgraph.graph;
  7242. delete subgraph.parent;
  7243. // register at the parent graph
  7244. if (!graph.subgraphs) {
  7245. graph.subgraphs = [];
  7246. }
  7247. graph.subgraphs.push(subgraph);
  7248. }
  7249. return subgraph;
  7250. }
  7251. /**
  7252. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7253. * Available keywords are 'node', 'edge', 'graph'.
  7254. * The previous list with default attributes will be replaced
  7255. * @param {Object} graph
  7256. * @returns {String | null} keyword Returns the name of the parsed attribute
  7257. * (node, edge, graph), or null if nothing
  7258. * is parsed.
  7259. */
  7260. function parseAttributeStatement (graph) {
  7261. // attribute statements
  7262. if (token == 'node') {
  7263. getToken();
  7264. // node attributes
  7265. graph.node = parseAttributeList();
  7266. return 'node';
  7267. }
  7268. else if (token == 'edge') {
  7269. getToken();
  7270. // edge attributes
  7271. graph.edge = parseAttributeList();
  7272. return 'edge';
  7273. }
  7274. else if (token == 'graph') {
  7275. getToken();
  7276. // graph attributes
  7277. graph.graph = parseAttributeList();
  7278. return 'graph';
  7279. }
  7280. return null;
  7281. }
  7282. /**
  7283. * parse a node statement
  7284. * @param {Object} graph
  7285. * @param {String | Number} id
  7286. */
  7287. function parseNodeStatement(graph, id) {
  7288. // node statement
  7289. var node = {
  7290. id: id
  7291. };
  7292. var attr = parseAttributeList();
  7293. if (attr) {
  7294. node.attr = attr;
  7295. }
  7296. addNode(graph, node);
  7297. // edge statements
  7298. parseEdge(graph, id);
  7299. }
  7300. /**
  7301. * Parse an edge or a series of edges
  7302. * @param {Object} graph
  7303. * @param {String | Number} from Id of the from node
  7304. */
  7305. function parseEdge(graph, from) {
  7306. while (token == '->' || token == '--') {
  7307. var to;
  7308. var type = token;
  7309. getToken();
  7310. var subgraph = parseSubgraph(graph);
  7311. if (subgraph) {
  7312. to = subgraph;
  7313. }
  7314. else {
  7315. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7316. throw newSyntaxError('Identifier or subgraph expected');
  7317. }
  7318. to = token;
  7319. addNode(graph, {
  7320. id: to
  7321. });
  7322. getToken();
  7323. }
  7324. // parse edge attributes
  7325. var attr = parseAttributeList();
  7326. // create edge
  7327. var edge = createEdge(graph, from, to, type, attr);
  7328. addEdge(graph, edge);
  7329. from = to;
  7330. }
  7331. }
  7332. /**
  7333. * Parse a set with attributes,
  7334. * for example [label="1.000", shape=solid]
  7335. * @return {Object | null} attr
  7336. */
  7337. function parseAttributeList() {
  7338. var attr = null;
  7339. while (token == '[') {
  7340. getToken();
  7341. attr = {};
  7342. while (token !== '' && token != ']') {
  7343. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7344. throw newSyntaxError('Attribute name expected');
  7345. }
  7346. var name = token;
  7347. getToken();
  7348. if (token != '=') {
  7349. throw newSyntaxError('Equal sign = expected');
  7350. }
  7351. getToken();
  7352. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7353. throw newSyntaxError('Attribute value expected');
  7354. }
  7355. var value = token;
  7356. setValue(attr, name, value); // name can be a path
  7357. getToken();
  7358. if (token ==',') {
  7359. getToken();
  7360. }
  7361. }
  7362. if (token != ']') {
  7363. throw newSyntaxError('Bracket ] expected');
  7364. }
  7365. getToken();
  7366. }
  7367. return attr;
  7368. }
  7369. /**
  7370. * Create a syntax error with extra information on current token and index.
  7371. * @param {String} message
  7372. * @returns {SyntaxError} err
  7373. */
  7374. function newSyntaxError(message) {
  7375. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7376. }
  7377. /**
  7378. * Chop off text after a maximum length
  7379. * @param {String} text
  7380. * @param {Number} maxLength
  7381. * @returns {String}
  7382. */
  7383. function chop (text, maxLength) {
  7384. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7385. }
  7386. /**
  7387. * Execute a function fn for each pair of elements in two arrays
  7388. * @param {Array | *} array1
  7389. * @param {Array | *} array2
  7390. * @param {function} fn
  7391. */
  7392. function forEach2(array1, array2, fn) {
  7393. if (array1 instanceof Array) {
  7394. array1.forEach(function (elem1) {
  7395. if (array2 instanceof Array) {
  7396. array2.forEach(function (elem2) {
  7397. fn(elem1, elem2);
  7398. });
  7399. }
  7400. else {
  7401. fn(elem1, array2);
  7402. }
  7403. });
  7404. }
  7405. else {
  7406. if (array2 instanceof Array) {
  7407. array2.forEach(function (elem2) {
  7408. fn(array1, elem2);
  7409. });
  7410. }
  7411. else {
  7412. fn(array1, array2);
  7413. }
  7414. }
  7415. }
  7416. /**
  7417. * Convert a string containing a graph in DOT language into a map containing
  7418. * with nodes and edges in the format of graph.
  7419. * @param {String} data Text containing a graph in DOT-notation
  7420. * @return {Object} graphData
  7421. */
  7422. function DOTToGraph (data) {
  7423. // parse the DOT file
  7424. var dotData = parseDOT(data);
  7425. var graphData = {
  7426. nodes: [],
  7427. edges: [],
  7428. options: {}
  7429. };
  7430. // copy the nodes
  7431. if (dotData.nodes) {
  7432. dotData.nodes.forEach(function (dotNode) {
  7433. var graphNode = {
  7434. id: dotNode.id,
  7435. label: String(dotNode.label || dotNode.id)
  7436. };
  7437. merge(graphNode, dotNode.attr);
  7438. if (graphNode.image) {
  7439. graphNode.shape = 'image';
  7440. }
  7441. graphData.nodes.push(graphNode);
  7442. });
  7443. }
  7444. // copy the edges
  7445. if (dotData.edges) {
  7446. /**
  7447. * Convert an edge in DOT format to an edge with VisGraph format
  7448. * @param {Object} dotEdge
  7449. * @returns {Object} graphEdge
  7450. */
  7451. function convertEdge(dotEdge) {
  7452. var graphEdge = {
  7453. from: dotEdge.from,
  7454. to: dotEdge.to
  7455. };
  7456. merge(graphEdge, dotEdge.attr);
  7457. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7458. return graphEdge;
  7459. }
  7460. dotData.edges.forEach(function (dotEdge) {
  7461. var from, to;
  7462. if (dotEdge.from instanceof Object) {
  7463. from = dotEdge.from.nodes;
  7464. }
  7465. else {
  7466. from = {
  7467. id: dotEdge.from
  7468. }
  7469. }
  7470. if (dotEdge.to instanceof Object) {
  7471. to = dotEdge.to.nodes;
  7472. }
  7473. else {
  7474. to = {
  7475. id: dotEdge.to
  7476. }
  7477. }
  7478. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7479. dotEdge.from.edges.forEach(function (subEdge) {
  7480. var graphEdge = convertEdge(subEdge);
  7481. graphData.edges.push(graphEdge);
  7482. });
  7483. }
  7484. forEach2(from, to, function (from, to) {
  7485. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7486. var graphEdge = convertEdge(subEdge);
  7487. graphData.edges.push(graphEdge);
  7488. });
  7489. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7490. dotEdge.to.edges.forEach(function (subEdge) {
  7491. var graphEdge = convertEdge(subEdge);
  7492. graphData.edges.push(graphEdge);
  7493. });
  7494. }
  7495. });
  7496. }
  7497. // copy the options
  7498. if (dotData.attr) {
  7499. graphData.options = dotData.attr;
  7500. }
  7501. return graphData;
  7502. }
  7503. // exports
  7504. exports.parseDOT = parseDOT;
  7505. exports.DOTToGraph = DOTToGraph;
  7506. })(typeof util !== 'undefined' ? util : exports);
  7507. /**
  7508. * Canvas shapes used by the Graph
  7509. */
  7510. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7511. /**
  7512. * Draw a circle shape
  7513. */
  7514. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7515. this.beginPath();
  7516. this.arc(x, y, r, 0, 2*Math.PI, false);
  7517. };
  7518. /**
  7519. * Draw a square shape
  7520. * @param {Number} x horizontal center
  7521. * @param {Number} y vertical center
  7522. * @param {Number} r size, width and height of the square
  7523. */
  7524. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7525. this.beginPath();
  7526. this.rect(x - r, y - r, r * 2, r * 2);
  7527. };
  7528. /**
  7529. * Draw a triangle shape
  7530. * @param {Number} x horizontal center
  7531. * @param {Number} y vertical center
  7532. * @param {Number} r radius, half the length of the sides of the triangle
  7533. */
  7534. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7535. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7536. this.beginPath();
  7537. var s = r * 2;
  7538. var s2 = s / 2;
  7539. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7540. var h = Math.sqrt(s * s - s2 * s2); // height
  7541. this.moveTo(x, y - (h - ir));
  7542. this.lineTo(x + s2, y + ir);
  7543. this.lineTo(x - s2, y + ir);
  7544. this.lineTo(x, y - (h - ir));
  7545. this.closePath();
  7546. };
  7547. /**
  7548. * Draw a triangle shape in downward orientation
  7549. * @param {Number} x horizontal center
  7550. * @param {Number} y vertical center
  7551. * @param {Number} r radius
  7552. */
  7553. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7554. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7555. this.beginPath();
  7556. var s = r * 2;
  7557. var s2 = s / 2;
  7558. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7559. var h = Math.sqrt(s * s - s2 * s2); // height
  7560. this.moveTo(x, y + (h - ir));
  7561. this.lineTo(x + s2, y - ir);
  7562. this.lineTo(x - s2, y - ir);
  7563. this.lineTo(x, y + (h - ir));
  7564. this.closePath();
  7565. };
  7566. /**
  7567. * Draw a star shape, a star with 5 points
  7568. * @param {Number} x horizontal center
  7569. * @param {Number} y vertical center
  7570. * @param {Number} r radius, half the length of the sides of the triangle
  7571. */
  7572. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7573. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7574. this.beginPath();
  7575. for (var n = 0; n < 10; n++) {
  7576. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7577. this.lineTo(
  7578. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7579. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7580. );
  7581. }
  7582. this.closePath();
  7583. };
  7584. /**
  7585. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7586. */
  7587. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7588. var r2d = Math.PI/180;
  7589. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7590. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7591. this.beginPath();
  7592. this.moveTo(x+r,y);
  7593. this.lineTo(x+w-r,y);
  7594. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7595. this.lineTo(x+w,y+h-r);
  7596. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7597. this.lineTo(x+r,y+h);
  7598. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7599. this.lineTo(x,y+r);
  7600. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7601. };
  7602. /**
  7603. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7604. */
  7605. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7606. var kappa = .5522848,
  7607. ox = (w / 2) * kappa, // control point offset horizontal
  7608. oy = (h / 2) * kappa, // control point offset vertical
  7609. xe = x + w, // x-end
  7610. ye = y + h, // y-end
  7611. xm = x + w / 2, // x-middle
  7612. ym = y + h / 2; // y-middle
  7613. this.beginPath();
  7614. this.moveTo(x, ym);
  7615. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7616. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7617. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7618. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7619. };
  7620. /**
  7621. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7622. */
  7623. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7624. var f = 1/3;
  7625. var wEllipse = w;
  7626. var hEllipse = h * f;
  7627. var kappa = .5522848,
  7628. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7629. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7630. xe = x + wEllipse, // x-end
  7631. ye = y + hEllipse, // y-end
  7632. xm = x + wEllipse / 2, // x-middle
  7633. ym = y + hEllipse / 2, // y-middle
  7634. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7635. yeb = y + h; // y-end, bottom ellipse
  7636. this.beginPath();
  7637. this.moveTo(xe, ym);
  7638. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7639. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7640. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7641. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7642. this.lineTo(xe, ymb);
  7643. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7644. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7645. this.lineTo(x, ym);
  7646. };
  7647. /**
  7648. * Draw an arrow point (no line)
  7649. */
  7650. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7651. // tail
  7652. var xt = x - length * Math.cos(angle);
  7653. var yt = y - length * Math.sin(angle);
  7654. // inner tail
  7655. // TODO: allow to customize different shapes
  7656. var xi = x - length * 0.9 * Math.cos(angle);
  7657. var yi = y - length * 0.9 * Math.sin(angle);
  7658. // left
  7659. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7660. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7661. // right
  7662. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7663. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7664. this.beginPath();
  7665. this.moveTo(x, y);
  7666. this.lineTo(xl, yl);
  7667. this.lineTo(xi, yi);
  7668. this.lineTo(xr, yr);
  7669. this.closePath();
  7670. };
  7671. /**
  7672. * Sets up the dashedLine functionality for drawing
  7673. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7674. * @author David Jordan
  7675. * @date 2012-08-08
  7676. */
  7677. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7678. if (!dashArray) dashArray=[10,5];
  7679. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7680. var dashCount = dashArray.length;
  7681. this.moveTo(x, y);
  7682. var dx = (x2-x), dy = (y2-y);
  7683. var slope = dy/dx;
  7684. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7685. var dashIndex=0, draw=true;
  7686. while (distRemaining>=0.1){
  7687. var dashLength = dashArray[dashIndex++%dashCount];
  7688. if (dashLength > distRemaining) dashLength = distRemaining;
  7689. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7690. if (dx<0) xStep = -xStep;
  7691. x += xStep;
  7692. y += slope*xStep;
  7693. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7694. distRemaining -= dashLength;
  7695. draw = !draw;
  7696. }
  7697. };
  7698. // TODO: add diamond shape
  7699. }
  7700. /**
  7701. * @class Node
  7702. * A node. A node can be connected to other nodes via one or multiple edges.
  7703. * @param {object} properties An object containing properties for the node. All
  7704. * properties are optional, except for the id.
  7705. * {number} id Id of the node. Required
  7706. * {string} label Text label for the node
  7707. * {number} x Horizontal position of the node
  7708. * {number} y Vertical position of the node
  7709. * {string} shape Node shape, available:
  7710. * "database", "circle", "ellipse",
  7711. * "box", "image", "text", "dot",
  7712. * "star", "triangle", "triangleDown",
  7713. * "square"
  7714. * {string} image An image url
  7715. * {string} title An title text, can be HTML
  7716. * {anytype} group A group name or number
  7717. * @param {Graph.Images} imagelist A list with images. Only needed
  7718. * when the node has an image
  7719. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7720. * retrieving group properties
  7721. * @param {Object} constants An object with default values for
  7722. * example for the color
  7723. *
  7724. */
  7725. function Node(properties, imagelist, grouplist, constants) {
  7726. this.selected = false;
  7727. this.edges = []; // all edges connected to this node
  7728. this.dynamicEdges = [];
  7729. this.reroutedEdges = {};
  7730. this.group = constants.nodes.group;
  7731. this.fontSize = constants.nodes.fontSize;
  7732. this.fontFace = constants.nodes.fontFace;
  7733. this.fontColor = constants.nodes.fontColor;
  7734. this.fontDrawThreshold = 3;
  7735. this.color = constants.nodes.color;
  7736. // set defaults for the properties
  7737. this.id = undefined;
  7738. this.shape = constants.nodes.shape;
  7739. this.image = constants.nodes.image;
  7740. this.x = null;
  7741. this.y = null;
  7742. this.xFixed = false;
  7743. this.yFixed = false;
  7744. this.horizontalAlignLeft = true; // these are for the navigation controls
  7745. this.verticalAlignTop = true; // these are for the navigation controls
  7746. this.radius = constants.nodes.radius;
  7747. this.baseRadiusValue = constants.nodes.radius;
  7748. this.radiusFixed = false;
  7749. this.radiusMin = constants.nodes.radiusMin;
  7750. this.radiusMax = constants.nodes.radiusMax;
  7751. this.level = -1;
  7752. this.preassignedLevel = false;
  7753. this.imagelist = imagelist;
  7754. this.grouplist = grouplist;
  7755. // physics properties
  7756. this.fx = 0.0; // external force x
  7757. this.fy = 0.0; // external force y
  7758. this.vx = 0.0; // velocity x
  7759. this.vy = 0.0; // velocity y
  7760. this.minForce = constants.minForce;
  7761. this.damping = constants.physics.damping;
  7762. this.mass = 1; // kg
  7763. this.fixedData = {x:null,y:null};
  7764. this.setProperties(properties, constants);
  7765. // creating the variables for clustering
  7766. this.resetCluster();
  7767. this.dynamicEdgesLength = 0;
  7768. this.clusterSession = 0;
  7769. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  7770. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  7771. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  7772. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  7773. this.growthIndicator = 0;
  7774. // variables to tell the node about the graph.
  7775. this.graphScaleInv = 1;
  7776. this.graphScale = 1;
  7777. this.canvasTopLeft = {"x": -300, "y": -300};
  7778. this.canvasBottomRight = {"x": 300, "y": 300};
  7779. this.parentEdgeId = null;
  7780. }
  7781. /**
  7782. * (re)setting the clustering variables and objects
  7783. */
  7784. Node.prototype.resetCluster = function() {
  7785. // clustering variables
  7786. this.formationScale = undefined; // this is used to determine when to open the cluster
  7787. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  7788. this.containedNodes = {};
  7789. this.containedEdges = {};
  7790. this.clusterSessions = [];
  7791. };
  7792. /**
  7793. * Attach a edge to the node
  7794. * @param {Edge} edge
  7795. */
  7796. Node.prototype.attachEdge = function(edge) {
  7797. if (this.edges.indexOf(edge) == -1) {
  7798. this.edges.push(edge);
  7799. }
  7800. if (this.dynamicEdges.indexOf(edge) == -1) {
  7801. this.dynamicEdges.push(edge);
  7802. }
  7803. this.dynamicEdgesLength = this.dynamicEdges.length;
  7804. };
  7805. /**
  7806. * Detach a edge from the node
  7807. * @param {Edge} edge
  7808. */
  7809. Node.prototype.detachEdge = function(edge) {
  7810. var index = this.edges.indexOf(edge);
  7811. if (index != -1) {
  7812. this.edges.splice(index, 1);
  7813. this.dynamicEdges.splice(index, 1);
  7814. }
  7815. this.dynamicEdgesLength = this.dynamicEdges.length;
  7816. };
  7817. /**
  7818. * Set or overwrite properties for the node
  7819. * @param {Object} properties an object with properties
  7820. * @param {Object} constants and object with default, global properties
  7821. */
  7822. Node.prototype.setProperties = function(properties, constants) {
  7823. if (!properties) {
  7824. return;
  7825. }
  7826. this.originalLabel = undefined;
  7827. // basic properties
  7828. if (properties.id !== undefined) {this.id = properties.id;}
  7829. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  7830. if (properties.title !== undefined) {this.title = properties.title;}
  7831. if (properties.group !== undefined) {this.group = properties.group;}
  7832. if (properties.x !== undefined) {this.x = properties.x;}
  7833. if (properties.y !== undefined) {this.y = properties.y;}
  7834. if (properties.value !== undefined) {this.value = properties.value;}
  7835. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  7836. // physics
  7837. if (properties.mass !== undefined) {this.mass = properties.mass;}
  7838. // navigation controls properties
  7839. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  7840. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  7841. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  7842. if (this.id === undefined) {
  7843. throw "Node must have an id";
  7844. }
  7845. // copy group properties
  7846. if (this.group) {
  7847. var groupObj = this.grouplist.get(this.group);
  7848. for (var prop in groupObj) {
  7849. if (groupObj.hasOwnProperty(prop)) {
  7850. this[prop] = groupObj[prop];
  7851. }
  7852. }
  7853. }
  7854. // individual shape properties
  7855. if (properties.shape !== undefined) {this.shape = properties.shape;}
  7856. if (properties.image !== undefined) {this.image = properties.image;}
  7857. if (properties.radius !== undefined) {this.radius = properties.radius;}
  7858. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  7859. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  7860. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  7861. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  7862. if (this.image !== undefined && this.image != "") {
  7863. if (this.imagelist) {
  7864. this.imageObj = this.imagelist.load(this.image);
  7865. }
  7866. else {
  7867. throw "No imagelist provided";
  7868. }
  7869. }
  7870. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  7871. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  7872. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  7873. if (this.shape == 'image') {
  7874. this.radiusMin = constants.nodes.widthMin;
  7875. this.radiusMax = constants.nodes.widthMax;
  7876. }
  7877. // choose draw method depending on the shape
  7878. switch (this.shape) {
  7879. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7880. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7881. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7882. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7883. // TODO: add diamond shape
  7884. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7885. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7886. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7887. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7888. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7889. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7890. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7891. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7892. }
  7893. // reset the size of the node, this can be changed
  7894. this._reset();
  7895. };
  7896. /**
  7897. * select this node
  7898. */
  7899. Node.prototype.select = function() {
  7900. this.selected = true;
  7901. this._reset();
  7902. };
  7903. /**
  7904. * unselect this node
  7905. */
  7906. Node.prototype.unselect = function() {
  7907. this.selected = false;
  7908. this._reset();
  7909. };
  7910. /**
  7911. * Reset the calculated size of the node, forces it to recalculate its size
  7912. */
  7913. Node.prototype.clearSizeCache = function() {
  7914. this._reset();
  7915. };
  7916. /**
  7917. * Reset the calculated size of the node, forces it to recalculate its size
  7918. * @private
  7919. */
  7920. Node.prototype._reset = function() {
  7921. this.width = undefined;
  7922. this.height = undefined;
  7923. };
  7924. /**
  7925. * get the title of this node.
  7926. * @return {string} title The title of the node, or undefined when no title
  7927. * has been set.
  7928. */
  7929. Node.prototype.getTitle = function() {
  7930. return typeof this.title === "function" ? this.title() : this.title;
  7931. };
  7932. /**
  7933. * Calculate the distance to the border of the Node
  7934. * @param {CanvasRenderingContext2D} ctx
  7935. * @param {Number} angle Angle in radians
  7936. * @returns {number} distance Distance to the border in pixels
  7937. */
  7938. Node.prototype.distanceToBorder = function (ctx, angle) {
  7939. var borderWidth = 1;
  7940. if (!this.width) {
  7941. this.resize(ctx);
  7942. }
  7943. switch (this.shape) {
  7944. case 'circle':
  7945. case 'dot':
  7946. return this.radius + borderWidth;
  7947. case 'ellipse':
  7948. var a = this.width / 2;
  7949. var b = this.height / 2;
  7950. var w = (Math.sin(angle) * a);
  7951. var h = (Math.cos(angle) * b);
  7952. return a * b / Math.sqrt(w * w + h * h);
  7953. // TODO: implement distanceToBorder for database
  7954. // TODO: implement distanceToBorder for triangle
  7955. // TODO: implement distanceToBorder for triangleDown
  7956. case 'box':
  7957. case 'image':
  7958. case 'text':
  7959. default:
  7960. if (this.width) {
  7961. return Math.min(
  7962. Math.abs(this.width / 2 / Math.cos(angle)),
  7963. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  7964. // TODO: reckon with border radius too in case of box
  7965. }
  7966. else {
  7967. return 0;
  7968. }
  7969. }
  7970. // TODO: implement calculation of distance to border for all shapes
  7971. };
  7972. /**
  7973. * Set forces acting on the node
  7974. * @param {number} fx Force in horizontal direction
  7975. * @param {number} fy Force in vertical direction
  7976. */
  7977. Node.prototype._setForce = function(fx, fy) {
  7978. this.fx = fx;
  7979. this.fy = fy;
  7980. };
  7981. /**
  7982. * Add forces acting on the node
  7983. * @param {number} fx Force in horizontal direction
  7984. * @param {number} fy Force in vertical direction
  7985. * @private
  7986. */
  7987. Node.prototype._addForce = function(fx, fy) {
  7988. this.fx += fx;
  7989. this.fy += fy;
  7990. };
  7991. /**
  7992. * Perform one discrete step for the node
  7993. * @param {number} interval Time interval in seconds
  7994. */
  7995. Node.prototype.discreteStep = function(interval) {
  7996. if (!this.xFixed) {
  7997. var dx = this.damping * this.vx; // damping force
  7998. var ax = (this.fx - dx) / this.mass; // acceleration
  7999. this.vx += ax * interval; // velocity
  8000. this.x += this.vx * interval; // position
  8001. }
  8002. if (!this.yFixed) {
  8003. var dy = this.damping * this.vy; // damping force
  8004. var ay = (this.fy - dy) / this.mass; // acceleration
  8005. this.vy += ay * interval; // velocity
  8006. this.y += this.vy * interval; // position
  8007. }
  8008. };
  8009. /**
  8010. * Perform one discrete step for the node
  8011. * @param {number} interval Time interval in seconds
  8012. */
  8013. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  8014. if (!this.xFixed) {
  8015. var dx = this.damping * this.vx; // damping force
  8016. var ax = (this.fx - dx) / this.mass; // acceleration
  8017. this.vx += ax * interval; // velocity
  8018. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  8019. this.x += this.vx * interval; // position
  8020. }
  8021. else {
  8022. this.fx = 0;
  8023. }
  8024. if (!this.yFixed) {
  8025. var dy = this.damping * this.vy; // damping force
  8026. var ay = (this.fy - dy) / this.mass; // acceleration
  8027. this.vy += ay * interval; // velocity
  8028. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  8029. this.y += this.vy * interval; // position
  8030. }
  8031. else {
  8032. this.fy = 0;
  8033. }
  8034. };
  8035. /**
  8036. * Check if this node has a fixed x and y position
  8037. * @return {boolean} true if fixed, false if not
  8038. */
  8039. Node.prototype.isFixed = function() {
  8040. return (this.xFixed && this.yFixed);
  8041. };
  8042. /**
  8043. * Check if this node is moving
  8044. * @param {number} vmin the minimum velocity considered as "moving"
  8045. * @return {boolean} true if moving, false if it has no velocity
  8046. */
  8047. // TODO: replace this method with calculating the kinetic energy
  8048. Node.prototype.isMoving = function(vmin) {
  8049. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8050. };
  8051. /**
  8052. * check if this node is selecte
  8053. * @return {boolean} selected True if node is selected, else false
  8054. */
  8055. Node.prototype.isSelected = function() {
  8056. return this.selected;
  8057. };
  8058. /**
  8059. * Retrieve the value of the node. Can be undefined
  8060. * @return {Number} value
  8061. */
  8062. Node.prototype.getValue = function() {
  8063. return this.value;
  8064. };
  8065. /**
  8066. * Calculate the distance from the nodes location to the given location (x,y)
  8067. * @param {Number} x
  8068. * @param {Number} y
  8069. * @return {Number} value
  8070. */
  8071. Node.prototype.getDistance = function(x, y) {
  8072. var dx = this.x - x,
  8073. dy = this.y - y;
  8074. return Math.sqrt(dx * dx + dy * dy);
  8075. };
  8076. /**
  8077. * Adjust the value range of the node. The node will adjust it's radius
  8078. * based on its value.
  8079. * @param {Number} min
  8080. * @param {Number} max
  8081. */
  8082. Node.prototype.setValueRange = function(min, max) {
  8083. if (!this.radiusFixed && this.value !== undefined) {
  8084. if (max == min) {
  8085. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8086. }
  8087. else {
  8088. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8089. this.radius = (this.value - min) * scale + this.radiusMin;
  8090. }
  8091. }
  8092. this.baseRadiusValue = this.radius;
  8093. };
  8094. /**
  8095. * Draw this node in the given canvas
  8096. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8097. * @param {CanvasRenderingContext2D} ctx
  8098. */
  8099. Node.prototype.draw = function(ctx) {
  8100. throw "Draw method not initialized for node";
  8101. };
  8102. /**
  8103. * Recalculate the size of this node in the given canvas
  8104. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8105. * @param {CanvasRenderingContext2D} ctx
  8106. */
  8107. Node.prototype.resize = function(ctx) {
  8108. throw "Resize method not initialized for node";
  8109. };
  8110. /**
  8111. * Check if this object is overlapping with the provided object
  8112. * @param {Object} obj an object with parameters left, top, right, bottom
  8113. * @return {boolean} True if location is located on node
  8114. */
  8115. Node.prototype.isOverlappingWith = function(obj) {
  8116. return (this.left < obj.right &&
  8117. this.left + this.width > obj.left &&
  8118. this.top < obj.bottom &&
  8119. this.top + this.height > obj.top);
  8120. };
  8121. Node.prototype._resizeImage = function (ctx) {
  8122. // TODO: pre calculate the image size
  8123. if (!this.width || !this.height) { // undefined or 0
  8124. var width, height;
  8125. if (this.value) {
  8126. this.radius = this.baseRadiusValue;
  8127. var scale = this.imageObj.height / this.imageObj.width;
  8128. if (scale !== undefined) {
  8129. width = this.radius || this.imageObj.width;
  8130. height = this.radius * scale || this.imageObj.height;
  8131. }
  8132. else {
  8133. width = 0;
  8134. height = 0;
  8135. }
  8136. }
  8137. else {
  8138. width = this.imageObj.width;
  8139. height = this.imageObj.height;
  8140. }
  8141. this.width = width;
  8142. this.height = height;
  8143. this.growthIndicator = 0;
  8144. if (this.width > 0 && this.height > 0) {
  8145. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8146. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8147. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8148. this.growthIndicator = this.width - width;
  8149. }
  8150. }
  8151. };
  8152. Node.prototype._drawImage = function (ctx) {
  8153. this._resizeImage(ctx);
  8154. this.left = this.x - this.width / 2;
  8155. this.top = this.y - this.height / 2;
  8156. var yLabel;
  8157. if (this.imageObj.width != 0 ) {
  8158. // draw the shade
  8159. if (this.clusterSize > 1) {
  8160. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8161. lineWidth *= this.graphScaleInv;
  8162. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8163. ctx.globalAlpha = 0.5;
  8164. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8165. }
  8166. // draw the image
  8167. ctx.globalAlpha = 1.0;
  8168. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8169. yLabel = this.y + this.height / 2;
  8170. }
  8171. else {
  8172. // image still loading... just draw the label for now
  8173. yLabel = this.y;
  8174. }
  8175. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8176. };
  8177. Node.prototype._resizeBox = function (ctx) {
  8178. if (!this.width) {
  8179. var margin = 5;
  8180. var textSize = this.getTextSize(ctx);
  8181. this.width = textSize.width + 2 * margin;
  8182. this.height = textSize.height + 2 * margin;
  8183. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8184. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8185. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8186. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8187. }
  8188. };
  8189. Node.prototype._drawBox = function (ctx) {
  8190. this._resizeBox(ctx);
  8191. this.left = this.x - this.width / 2;
  8192. this.top = this.y - this.height / 2;
  8193. var clusterLineWidth = 2.5;
  8194. var selectionLineWidth = 2;
  8195. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8196. // draw the outer border
  8197. if (this.clusterSize > 1) {
  8198. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8199. ctx.lineWidth *= this.graphScaleInv;
  8200. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8201. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8202. ctx.stroke();
  8203. }
  8204. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8205. ctx.lineWidth *= this.graphScaleInv;
  8206. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8207. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8208. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8209. ctx.fill();
  8210. ctx.stroke();
  8211. this._label(ctx, this.label, this.x, this.y);
  8212. };
  8213. Node.prototype._resizeDatabase = function (ctx) {
  8214. if (!this.width) {
  8215. var margin = 5;
  8216. var textSize = this.getTextSize(ctx);
  8217. var size = textSize.width + 2 * margin;
  8218. this.width = size;
  8219. this.height = size;
  8220. // scaling used for clustering
  8221. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8222. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8223. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8224. this.growthIndicator = this.width - size;
  8225. }
  8226. };
  8227. Node.prototype._drawDatabase = function (ctx) {
  8228. this._resizeDatabase(ctx);
  8229. this.left = this.x - this.width / 2;
  8230. this.top = this.y - this.height / 2;
  8231. var clusterLineWidth = 2.5;
  8232. var selectionLineWidth = 2;
  8233. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8234. // draw the outer border
  8235. if (this.clusterSize > 1) {
  8236. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8237. ctx.lineWidth *= this.graphScaleInv;
  8238. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8239. 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);
  8240. ctx.stroke();
  8241. }
  8242. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8243. ctx.lineWidth *= this.graphScaleInv;
  8244. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8245. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8246. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8247. ctx.fill();
  8248. ctx.stroke();
  8249. this._label(ctx, this.label, this.x, this.y);
  8250. };
  8251. Node.prototype._resizeCircle = function (ctx) {
  8252. if (!this.width) {
  8253. var margin = 5;
  8254. var textSize = this.getTextSize(ctx);
  8255. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8256. this.radius = diameter / 2;
  8257. this.width = diameter;
  8258. this.height = diameter;
  8259. // scaling used for clustering
  8260. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8261. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8262. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8263. this.growthIndicator = this.radius - 0.5*diameter;
  8264. }
  8265. };
  8266. Node.prototype._drawCircle = function (ctx) {
  8267. this._resizeCircle(ctx);
  8268. this.left = this.x - this.width / 2;
  8269. this.top = this.y - this.height / 2;
  8270. var clusterLineWidth = 2.5;
  8271. var selectionLineWidth = 2;
  8272. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8273. // draw the outer border
  8274. if (this.clusterSize > 1) {
  8275. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8276. ctx.lineWidth *= this.graphScaleInv;
  8277. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8278. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8279. ctx.stroke();
  8280. }
  8281. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8282. ctx.lineWidth *= this.graphScaleInv;
  8283. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8284. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8285. ctx.circle(this.x, this.y, this.radius);
  8286. ctx.fill();
  8287. ctx.stroke();
  8288. this._label(ctx, this.label, this.x, this.y);
  8289. };
  8290. Node.prototype._resizeEllipse = function (ctx) {
  8291. if (!this.width) {
  8292. var textSize = this.getTextSize(ctx);
  8293. this.width = textSize.width * 1.5;
  8294. this.height = textSize.height * 2;
  8295. if (this.width < this.height) {
  8296. this.width = this.height;
  8297. }
  8298. var defaultSize = this.width;
  8299. // scaling used for clustering
  8300. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8301. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8302. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8303. this.growthIndicator = this.width - defaultSize;
  8304. }
  8305. };
  8306. Node.prototype._drawEllipse = function (ctx) {
  8307. this._resizeEllipse(ctx);
  8308. this.left = this.x - this.width / 2;
  8309. this.top = this.y - this.height / 2;
  8310. var clusterLineWidth = 2.5;
  8311. var selectionLineWidth = 2;
  8312. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8313. // draw the outer border
  8314. if (this.clusterSize > 1) {
  8315. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8316. ctx.lineWidth *= this.graphScaleInv;
  8317. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8318. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8319. ctx.stroke();
  8320. }
  8321. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8322. ctx.lineWidth *= this.graphScaleInv;
  8323. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8324. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8325. ctx.ellipse(this.left, this.top, this.width, this.height);
  8326. ctx.fill();
  8327. ctx.stroke();
  8328. this._label(ctx, this.label, this.x, this.y);
  8329. };
  8330. Node.prototype._drawDot = function (ctx) {
  8331. this._drawShape(ctx, 'circle');
  8332. };
  8333. Node.prototype._drawTriangle = function (ctx) {
  8334. this._drawShape(ctx, 'triangle');
  8335. };
  8336. Node.prototype._drawTriangleDown = function (ctx) {
  8337. this._drawShape(ctx, 'triangleDown');
  8338. };
  8339. Node.prototype._drawSquare = function (ctx) {
  8340. this._drawShape(ctx, 'square');
  8341. };
  8342. Node.prototype._drawStar = function (ctx) {
  8343. this._drawShape(ctx, 'star');
  8344. };
  8345. Node.prototype._resizeShape = function (ctx) {
  8346. if (!this.width) {
  8347. this.radius = this.baseRadiusValue;
  8348. var size = 2 * this.radius;
  8349. this.width = size;
  8350. this.height = size;
  8351. // scaling used for clustering
  8352. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8353. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8354. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8355. this.growthIndicator = this.width - size;
  8356. }
  8357. };
  8358. Node.prototype._drawShape = function (ctx, shape) {
  8359. this._resizeShape(ctx);
  8360. this.left = this.x - this.width / 2;
  8361. this.top = this.y - this.height / 2;
  8362. var clusterLineWidth = 2.5;
  8363. var selectionLineWidth = 2;
  8364. var radiusMultiplier = 2;
  8365. // choose draw method depending on the shape
  8366. switch (shape) {
  8367. case 'dot': radiusMultiplier = 2; break;
  8368. case 'square': radiusMultiplier = 2; break;
  8369. case 'triangle': radiusMultiplier = 3; break;
  8370. case 'triangleDown': radiusMultiplier = 3; break;
  8371. case 'star': radiusMultiplier = 4; break;
  8372. }
  8373. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8374. // draw the outer border
  8375. if (this.clusterSize > 1) {
  8376. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8377. ctx.lineWidth *= this.graphScaleInv;
  8378. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8379. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8380. ctx.stroke();
  8381. }
  8382. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8383. ctx.lineWidth *= this.graphScaleInv;
  8384. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8385. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8386. ctx[shape](this.x, this.y, this.radius);
  8387. ctx.fill();
  8388. ctx.stroke();
  8389. if (this.label) {
  8390. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8391. }
  8392. };
  8393. Node.prototype._resizeText = function (ctx) {
  8394. if (!this.width) {
  8395. var margin = 5;
  8396. var textSize = this.getTextSize(ctx);
  8397. this.width = textSize.width + 2 * margin;
  8398. this.height = textSize.height + 2 * margin;
  8399. // scaling used for clustering
  8400. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8401. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8402. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8403. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8404. }
  8405. };
  8406. Node.prototype._drawText = function (ctx) {
  8407. this._resizeText(ctx);
  8408. this.left = this.x - this.width / 2;
  8409. this.top = this.y - this.height / 2;
  8410. this._label(ctx, this.label, this.x, this.y);
  8411. };
  8412. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8413. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  8414. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8415. ctx.fillStyle = this.fontColor || "black";
  8416. ctx.textAlign = align || "center";
  8417. ctx.textBaseline = baseline || "middle";
  8418. var lines = text.split('\n'),
  8419. lineCount = lines.length,
  8420. fontSize = (this.fontSize + 4),
  8421. yLine = y + (1 - lineCount) / 2 * fontSize;
  8422. for (var i = 0; i < lineCount; i++) {
  8423. ctx.fillText(lines[i], x, yLine);
  8424. yLine += fontSize;
  8425. }
  8426. }
  8427. };
  8428. Node.prototype.getTextSize = function(ctx) {
  8429. if (this.label !== undefined) {
  8430. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8431. var lines = this.label.split('\n'),
  8432. height = (this.fontSize + 4) * lines.length,
  8433. width = 0;
  8434. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8435. width = Math.max(width, ctx.measureText(lines[i]).width);
  8436. }
  8437. return {"width": width, "height": height};
  8438. }
  8439. else {
  8440. return {"width": 0, "height": 0};
  8441. }
  8442. };
  8443. /**
  8444. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  8445. * there is a safety margin of 0.3 * width;
  8446. *
  8447. * @returns {boolean}
  8448. */
  8449. Node.prototype.inArea = function() {
  8450. if (this.width !== undefined) {
  8451. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  8452. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  8453. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  8454. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  8455. }
  8456. else {
  8457. return true;
  8458. }
  8459. };
  8460. /**
  8461. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  8462. * @returns {boolean}
  8463. */
  8464. Node.prototype.inView = function() {
  8465. return (this.x >= this.canvasTopLeft.x &&
  8466. this.x < this.canvasBottomRight.x &&
  8467. this.y >= this.canvasTopLeft.y &&
  8468. this.y < this.canvasBottomRight.y);
  8469. };
  8470. /**
  8471. * This allows the zoom level of the graph to influence the rendering
  8472. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  8473. *
  8474. * @param scale
  8475. * @param canvasTopLeft
  8476. * @param canvasBottomRight
  8477. */
  8478. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  8479. this.graphScaleInv = 1.0/scale;
  8480. this.graphScale = scale;
  8481. this.canvasTopLeft = canvasTopLeft;
  8482. this.canvasBottomRight = canvasBottomRight;
  8483. };
  8484. /**
  8485. * This allows the zoom level of the graph to influence the rendering
  8486. *
  8487. * @param scale
  8488. */
  8489. Node.prototype.setScale = function(scale) {
  8490. this.graphScaleInv = 1.0/scale;
  8491. this.graphScale = scale;
  8492. };
  8493. /**
  8494. * set the velocity at 0. Is called when this node is contained in another during clustering
  8495. */
  8496. Node.prototype.clearVelocity = function() {
  8497. this.vx = 0;
  8498. this.vy = 0;
  8499. };
  8500. /**
  8501. * Basic preservation of (kinectic) energy
  8502. *
  8503. * @param massBeforeClustering
  8504. */
  8505. Node.prototype.updateVelocity = function(massBeforeClustering) {
  8506. var energyBefore = this.vx * this.vx * massBeforeClustering;
  8507. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8508. this.vx = Math.sqrt(energyBefore/this.mass);
  8509. energyBefore = this.vy * this.vy * massBeforeClustering;
  8510. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8511. this.vy = Math.sqrt(energyBefore/this.mass);
  8512. };
  8513. /**
  8514. * @class Edge
  8515. *
  8516. * A edge connects two nodes
  8517. * @param {Object} properties Object with properties. Must contain
  8518. * At least properties from and to.
  8519. * Available properties: from (number),
  8520. * to (number), label (string, color (string),
  8521. * width (number), style (string),
  8522. * length (number), title (string)
  8523. * @param {Graph} graph A graph object, used to find and edge to
  8524. * nodes.
  8525. * @param {Object} constants An object with default values for
  8526. * example for the color
  8527. */
  8528. function Edge (properties, graph, constants) {
  8529. if (!graph) {
  8530. throw "No graph provided";
  8531. }
  8532. this.graph = graph;
  8533. // initialize constants
  8534. this.widthMin = constants.edges.widthMin;
  8535. this.widthMax = constants.edges.widthMax;
  8536. // initialize variables
  8537. this.id = undefined;
  8538. this.fromId = undefined;
  8539. this.toId = undefined;
  8540. this.style = constants.edges.style;
  8541. this.title = undefined;
  8542. this.width = constants.edges.width;
  8543. this.value = undefined;
  8544. this.length = constants.physics.springLength;
  8545. this.customLength = false;
  8546. this.selected = false;
  8547. this.smooth = constants.smoothCurves;
  8548. this.from = null; // a node
  8549. this.to = null; // a node
  8550. this.via = null; // a temp node
  8551. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  8552. // by storing the original information we can revert to the original connection when the cluser is opened.
  8553. this.originalFromId = [];
  8554. this.originalToId = [];
  8555. this.connected = false;
  8556. // Added to support dashed lines
  8557. // David Jordan
  8558. // 2012-08-08
  8559. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8560. this.color = {color:constants.edges.color.color,
  8561. highlight:constants.edges.color.highlight};
  8562. this.widthFixed = false;
  8563. this.lengthFixed = false;
  8564. this.setProperties(properties, constants);
  8565. }
  8566. /**
  8567. * Set or overwrite properties for the edge
  8568. * @param {Object} properties an object with properties
  8569. * @param {Object} constants and object with default, global properties
  8570. */
  8571. Edge.prototype.setProperties = function(properties, constants) {
  8572. if (!properties) {
  8573. return;
  8574. }
  8575. if (properties.from !== undefined) {this.fromId = properties.from;}
  8576. if (properties.to !== undefined) {this.toId = properties.to;}
  8577. if (properties.id !== undefined) {this.id = properties.id;}
  8578. if (properties.style !== undefined) {this.style = properties.style;}
  8579. if (properties.label !== undefined) {this.label = properties.label;}
  8580. if (this.label) {
  8581. this.fontSize = constants.edges.fontSize;
  8582. this.fontFace = constants.edges.fontFace;
  8583. this.fontColor = constants.edges.fontColor;
  8584. this.fontFill = constants.edges.fontFill;
  8585. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8586. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8587. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8588. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  8589. }
  8590. if (properties.title !== undefined) {this.title = properties.title;}
  8591. if (properties.width !== undefined) {this.width = properties.width;}
  8592. if (properties.value !== undefined) {this.value = properties.value;}
  8593. if (properties.length !== undefined) {this.length = properties.length;
  8594. this.customLength = true;}
  8595. // Added to support dashed lines
  8596. // David Jordan
  8597. // 2012-08-08
  8598. if (properties.dash) {
  8599. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  8600. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  8601. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  8602. }
  8603. if (properties.color !== undefined) {
  8604. if (util.isString(properties.color)) {
  8605. this.color.color = properties.color;
  8606. this.color.highlight = properties.color;
  8607. }
  8608. else {
  8609. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  8610. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  8611. }
  8612. }
  8613. // A node is connected when it has a from and to node.
  8614. this.connect();
  8615. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  8616. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  8617. // set draw method based on style
  8618. switch (this.style) {
  8619. case 'line': this.draw = this._drawLine; break;
  8620. case 'arrow': this.draw = this._drawArrow; break;
  8621. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8622. case 'dash-line': this.draw = this._drawDashLine; break;
  8623. default: this.draw = this._drawLine; break;
  8624. }
  8625. };
  8626. /**
  8627. * Connect an edge to its nodes
  8628. */
  8629. Edge.prototype.connect = function () {
  8630. this.disconnect();
  8631. this.from = this.graph.nodes[this.fromId] || null;
  8632. this.to = this.graph.nodes[this.toId] || null;
  8633. this.connected = (this.from && this.to);
  8634. if (this.connected) {
  8635. this.from.attachEdge(this);
  8636. this.to.attachEdge(this);
  8637. }
  8638. else {
  8639. if (this.from) {
  8640. this.from.detachEdge(this);
  8641. }
  8642. if (this.to) {
  8643. this.to.detachEdge(this);
  8644. }
  8645. }
  8646. };
  8647. /**
  8648. * Disconnect an edge from its nodes
  8649. */
  8650. Edge.prototype.disconnect = function () {
  8651. if (this.from) {
  8652. this.from.detachEdge(this);
  8653. this.from = null;
  8654. }
  8655. if (this.to) {
  8656. this.to.detachEdge(this);
  8657. this.to = null;
  8658. }
  8659. this.connected = false;
  8660. };
  8661. /**
  8662. * get the title of this edge.
  8663. * @return {string} title The title of the edge, or undefined when no title
  8664. * has been set.
  8665. */
  8666. Edge.prototype.getTitle = function() {
  8667. return typeof this.title === "function" ? this.title() : this.title;
  8668. };
  8669. /**
  8670. * Retrieve the value of the edge. Can be undefined
  8671. * @return {Number} value
  8672. */
  8673. Edge.prototype.getValue = function() {
  8674. return this.value;
  8675. };
  8676. /**
  8677. * Adjust the value range of the edge. The edge will adjust it's width
  8678. * based on its value.
  8679. * @param {Number} min
  8680. * @param {Number} max
  8681. */
  8682. Edge.prototype.setValueRange = function(min, max) {
  8683. if (!this.widthFixed && this.value !== undefined) {
  8684. var scale = (this.widthMax - this.widthMin) / (max - min);
  8685. this.width = (this.value - min) * scale + this.widthMin;
  8686. }
  8687. };
  8688. /**
  8689. * Redraw a edge
  8690. * Draw this edge in the given canvas
  8691. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8692. * @param {CanvasRenderingContext2D} ctx
  8693. */
  8694. Edge.prototype.draw = function(ctx) {
  8695. throw "Method draw not initialized in edge";
  8696. };
  8697. /**
  8698. * Check if this object is overlapping with the provided object
  8699. * @param {Object} obj an object with parameters left, top
  8700. * @return {boolean} True if location is located on the edge
  8701. */
  8702. Edge.prototype.isOverlappingWith = function(obj) {
  8703. if (this.connected) {
  8704. var distMax = 10;
  8705. var xFrom = this.from.x;
  8706. var yFrom = this.from.y;
  8707. var xTo = this.to.x;
  8708. var yTo = this.to.y;
  8709. var xObj = obj.left;
  8710. var yObj = obj.top;
  8711. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8712. return (dist < distMax);
  8713. }
  8714. else {
  8715. return false
  8716. }
  8717. };
  8718. /**
  8719. * Redraw a edge as a line
  8720. * Draw this edge in the given canvas
  8721. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8722. * @param {CanvasRenderingContext2D} ctx
  8723. * @private
  8724. */
  8725. Edge.prototype._drawLine = function(ctx) {
  8726. // set style
  8727. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8728. else {ctx.strokeStyle = this.color.color;}
  8729. ctx.lineWidth = this._getLineWidth();
  8730. if (this.from != this.to) {
  8731. // draw line
  8732. this._line(ctx);
  8733. // draw label
  8734. var point;
  8735. if (this.label) {
  8736. if (this.smooth == true) {
  8737. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  8738. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  8739. point = {x:midpointX, y:midpointY};
  8740. }
  8741. else {
  8742. point = this._pointOnLine(0.5);
  8743. }
  8744. this._label(ctx, this.label, point.x, point.y);
  8745. }
  8746. }
  8747. else {
  8748. var x, y;
  8749. var radius = this.length / 4;
  8750. var node = this.from;
  8751. if (!node.width) {
  8752. node.resize(ctx);
  8753. }
  8754. if (node.width > node.height) {
  8755. x = node.x + node.width / 2;
  8756. y = node.y - radius;
  8757. }
  8758. else {
  8759. x = node.x + radius;
  8760. y = node.y - node.height / 2;
  8761. }
  8762. this._circle(ctx, x, y, radius);
  8763. point = this._pointOnCircle(x, y, radius, 0.5);
  8764. this._label(ctx, this.label, point.x, point.y);
  8765. }
  8766. };
  8767. /**
  8768. * Get the line width of the edge. Depends on width and whether one of the
  8769. * connected nodes is selected.
  8770. * @return {Number} width
  8771. * @private
  8772. */
  8773. Edge.prototype._getLineWidth = function() {
  8774. if (this.selected == true) {
  8775. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  8776. }
  8777. else {
  8778. return this.width*this.graphScaleInv;
  8779. }
  8780. };
  8781. /**
  8782. * Draw a line between two nodes
  8783. * @param {CanvasRenderingContext2D} ctx
  8784. * @private
  8785. */
  8786. Edge.prototype._line = function (ctx) {
  8787. // draw a straight line
  8788. ctx.beginPath();
  8789. ctx.moveTo(this.from.x, this.from.y);
  8790. if (this.smooth == true) {
  8791. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  8792. }
  8793. else {
  8794. ctx.lineTo(this.to.x, this.to.y);
  8795. }
  8796. ctx.stroke();
  8797. };
  8798. /**
  8799. * Draw a line from a node to itself, a circle
  8800. * @param {CanvasRenderingContext2D} ctx
  8801. * @param {Number} x
  8802. * @param {Number} y
  8803. * @param {Number} radius
  8804. * @private
  8805. */
  8806. Edge.prototype._circle = function (ctx, x, y, radius) {
  8807. // draw a circle
  8808. ctx.beginPath();
  8809. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8810. ctx.stroke();
  8811. };
  8812. /**
  8813. * Draw label with white background and with the middle at (x, y)
  8814. * @param {CanvasRenderingContext2D} ctx
  8815. * @param {String} text
  8816. * @param {Number} x
  8817. * @param {Number} y
  8818. * @private
  8819. */
  8820. Edge.prototype._label = function (ctx, text, x, y) {
  8821. if (text) {
  8822. // TODO: cache the calculated size
  8823. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8824. this.fontSize + "px " + this.fontFace;
  8825. ctx.fillStyle = this.fontFill;
  8826. var width = ctx.measureText(text).width;
  8827. var height = this.fontSize;
  8828. var left = x - width / 2;
  8829. var top = y - height / 2;
  8830. ctx.fillRect(left, top, width, height);
  8831. // draw text
  8832. ctx.fillStyle = this.fontColor || "black";
  8833. ctx.textAlign = "left";
  8834. ctx.textBaseline = "top";
  8835. ctx.fillText(text, left, top);
  8836. }
  8837. };
  8838. /**
  8839. * Redraw a edge as a dashed line
  8840. * Draw this edge in the given canvas
  8841. * @author David Jordan
  8842. * @date 2012-08-08
  8843. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8844. * @param {CanvasRenderingContext2D} ctx
  8845. * @private
  8846. */
  8847. Edge.prototype._drawDashLine = function(ctx) {
  8848. // set style
  8849. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8850. else {ctx.strokeStyle = this.color.color;}
  8851. ctx.lineWidth = this._getLineWidth();
  8852. // only firefox and chrome support this method, else we use the legacy one.
  8853. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  8854. ctx.beginPath();
  8855. ctx.moveTo(this.from.x, this.from.y);
  8856. // configure the dash pattern
  8857. var pattern = [0];
  8858. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  8859. pattern = [this.dash.length,this.dash.gap];
  8860. }
  8861. else {
  8862. pattern = [5,5];
  8863. }
  8864. // set dash settings for chrome or firefox
  8865. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  8866. ctx.setLineDash(pattern);
  8867. ctx.lineDashOffset = 0;
  8868. } else { //Firefox
  8869. ctx.mozDash = pattern;
  8870. ctx.mozDashOffset = 0;
  8871. }
  8872. // draw the line
  8873. if (this.smooth == true) {
  8874. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  8875. }
  8876. else {
  8877. ctx.lineTo(this.to.x, this.to.y);
  8878. }
  8879. ctx.stroke();
  8880. // restore the dash settings.
  8881. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  8882. ctx.setLineDash([0]);
  8883. ctx.lineDashOffset = 0;
  8884. } else { //Firefox
  8885. ctx.mozDash = [0];
  8886. ctx.mozDashOffset = 0;
  8887. }
  8888. }
  8889. else { // unsupporting smooth lines
  8890. // draw dashed line
  8891. ctx.beginPath();
  8892. ctx.lineCap = 'round';
  8893. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  8894. {
  8895. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8896. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  8897. }
  8898. 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
  8899. {
  8900. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8901. [this.dash.length,this.dash.gap]);
  8902. }
  8903. else //If all else fails draw a line
  8904. {
  8905. ctx.moveTo(this.from.x, this.from.y);
  8906. ctx.lineTo(this.to.x, this.to.y);
  8907. }
  8908. ctx.stroke();
  8909. }
  8910. // draw label
  8911. if (this.label) {
  8912. var point;
  8913. if (this.smooth == true) {
  8914. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  8915. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  8916. point = {x:midpointX, y:midpointY};
  8917. }
  8918. else {
  8919. point = this._pointOnLine(0.5);
  8920. }
  8921. this._label(ctx, this.label, point.x, point.y);
  8922. }
  8923. };
  8924. /**
  8925. * Get a point on a line
  8926. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8927. * @return {Object} point
  8928. * @private
  8929. */
  8930. Edge.prototype._pointOnLine = function (percentage) {
  8931. return {
  8932. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  8933. y: (1 - percentage) * this.from.y + percentage * this.to.y
  8934. }
  8935. };
  8936. /**
  8937. * Get a point on a circle
  8938. * @param {Number} x
  8939. * @param {Number} y
  8940. * @param {Number} radius
  8941. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8942. * @return {Object} point
  8943. * @private
  8944. */
  8945. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  8946. var angle = (percentage - 3/8) * 2 * Math.PI;
  8947. return {
  8948. x: x + radius * Math.cos(angle),
  8949. y: y - radius * Math.sin(angle)
  8950. }
  8951. };
  8952. /**
  8953. * Redraw a edge as a line with an arrow halfway the line
  8954. * Draw this edge in the given canvas
  8955. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8956. * @param {CanvasRenderingContext2D} ctx
  8957. * @private
  8958. */
  8959. Edge.prototype._drawArrowCenter = function(ctx) {
  8960. var point;
  8961. // set style
  8962. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  8963. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  8964. ctx.lineWidth = this._getLineWidth();
  8965. if (this.from != this.to) {
  8966. // draw line
  8967. this._line(ctx);
  8968. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8969. var length = 10 + 5 * this.width; // TODO: make customizable?
  8970. // draw an arrow halfway the line
  8971. if (this.smooth == true) {
  8972. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  8973. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  8974. point = {x:midpointX, y:midpointY};
  8975. }
  8976. else {
  8977. point = this._pointOnLine(0.5);
  8978. }
  8979. ctx.arrow(point.x, point.y, angle, length);
  8980. ctx.fill();
  8981. ctx.stroke();
  8982. // draw label
  8983. if (this.label) {
  8984. this._label(ctx, this.label, point.x, point.y);
  8985. }
  8986. }
  8987. else {
  8988. // draw circle
  8989. var x, y;
  8990. var radius = 0.25 * Math.max(100,this.length);
  8991. var node = this.from;
  8992. if (!node.width) {
  8993. node.resize(ctx);
  8994. }
  8995. if (node.width > node.height) {
  8996. x = node.x + node.width * 0.5;
  8997. y = node.y - radius;
  8998. }
  8999. else {
  9000. x = node.x + radius;
  9001. y = node.y - node.height * 0.5;
  9002. }
  9003. this._circle(ctx, x, y, radius);
  9004. // draw all arrows
  9005. var angle = 0.2 * Math.PI;
  9006. var length = 10 + 5 * this.width; // TODO: make customizable?
  9007. point = this._pointOnCircle(x, y, radius, 0.5);
  9008. ctx.arrow(point.x, point.y, angle, length);
  9009. ctx.fill();
  9010. ctx.stroke();
  9011. // draw label
  9012. if (this.label) {
  9013. point = this._pointOnCircle(x, y, radius, 0.5);
  9014. this._label(ctx, this.label, point.x, point.y);
  9015. }
  9016. }
  9017. };
  9018. /**
  9019. * Redraw a edge as a line with an arrow
  9020. * Draw this edge in the given canvas
  9021. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9022. * @param {CanvasRenderingContext2D} ctx
  9023. * @private
  9024. */
  9025. Edge.prototype._drawArrow = function(ctx) {
  9026. // set style
  9027. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9028. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9029. ctx.lineWidth = this._getLineWidth();
  9030. var angle, length;
  9031. //draw a line
  9032. if (this.from != this.to) {
  9033. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9034. var dx = (this.to.x - this.from.x);
  9035. var dy = (this.to.y - this.from.y);
  9036. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9037. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  9038. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  9039. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  9040. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  9041. if (this.smooth == true) {
  9042. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  9043. dx = (this.to.x - this.via.x);
  9044. dy = (this.to.y - this.via.y);
  9045. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9046. }
  9047. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  9048. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  9049. var xTo,yTo;
  9050. if (this.smooth == true) {
  9051. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  9052. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  9053. }
  9054. else {
  9055. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  9056. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  9057. }
  9058. ctx.beginPath();
  9059. ctx.moveTo(xFrom,yFrom);
  9060. if (this.smooth == true) {
  9061. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  9062. }
  9063. else {
  9064. ctx.lineTo(xTo, yTo);
  9065. }
  9066. ctx.stroke();
  9067. // draw arrow at the end of the line
  9068. length = 10 + 5 * this.width;
  9069. ctx.arrow(xTo, yTo, angle, length);
  9070. ctx.fill();
  9071. ctx.stroke();
  9072. // draw label
  9073. if (this.label) {
  9074. var point;
  9075. if (this.smooth == true) {
  9076. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9077. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9078. point = {x:midpointX, y:midpointY};
  9079. }
  9080. else {
  9081. point = this._pointOnLine(0.5);
  9082. }
  9083. this._label(ctx, this.label, point.x, point.y);
  9084. }
  9085. }
  9086. else {
  9087. // draw circle
  9088. var node = this.from;
  9089. var x, y, arrow;
  9090. var radius = 0.25 * Math.max(100,this.length);
  9091. if (!node.width) {
  9092. node.resize(ctx);
  9093. }
  9094. if (node.width > node.height) {
  9095. x = node.x + node.width * 0.5;
  9096. y = node.y - radius;
  9097. arrow = {
  9098. x: x,
  9099. y: node.y,
  9100. angle: 0.9 * Math.PI
  9101. };
  9102. }
  9103. else {
  9104. x = node.x + radius;
  9105. y = node.y - node.height * 0.5;
  9106. arrow = {
  9107. x: node.x,
  9108. y: y,
  9109. angle: 0.6 * Math.PI
  9110. };
  9111. }
  9112. ctx.beginPath();
  9113. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9114. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9115. ctx.stroke();
  9116. // draw all arrows
  9117. length = 10 + 5 * this.width; // TODO: make customizable?
  9118. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9119. ctx.fill();
  9120. ctx.stroke();
  9121. // draw label
  9122. if (this.label) {
  9123. point = this._pointOnCircle(x, y, radius, 0.5);
  9124. this._label(ctx, this.label, point.x, point.y);
  9125. }
  9126. }
  9127. };
  9128. /**
  9129. * Calculate the distance between a point (x3,y3) and a line segment from
  9130. * (x1,y1) to (x2,y2).
  9131. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9132. * @param {number} x1
  9133. * @param {number} y1
  9134. * @param {number} x2
  9135. * @param {number} y2
  9136. * @param {number} x3
  9137. * @param {number} y3
  9138. * @private
  9139. */
  9140. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9141. if (this.smooth == true) {
  9142. var minDistance = 1e9;
  9143. var i,t,x,y,dx,dy;
  9144. for (i = 0; i < 10; i++) {
  9145. t = 0.1*i;
  9146. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  9147. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  9148. dx = Math.abs(x3-x);
  9149. dy = Math.abs(y3-y);
  9150. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  9151. }
  9152. return minDistance
  9153. }
  9154. else {
  9155. var px = x2-x1,
  9156. py = y2-y1,
  9157. something = px*px + py*py,
  9158. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9159. if (u > 1) {
  9160. u = 1;
  9161. }
  9162. else if (u < 0) {
  9163. u = 0;
  9164. }
  9165. var x = x1 + u * px,
  9166. y = y1 + u * py,
  9167. dx = x - x3,
  9168. dy = y - y3;
  9169. //# Note: If the actual distance does not matter,
  9170. //# if you only want to compare what this function
  9171. //# returns to other results of this function, you
  9172. //# can just return the squared distance instead
  9173. //# (i.e. remove the sqrt) to gain a little performance
  9174. return Math.sqrt(dx*dx + dy*dy);
  9175. }
  9176. };
  9177. /**
  9178. * This allows the zoom level of the graph to influence the rendering
  9179. *
  9180. * @param scale
  9181. */
  9182. Edge.prototype.setScale = function(scale) {
  9183. this.graphScaleInv = 1.0/scale;
  9184. };
  9185. Edge.prototype.select = function() {
  9186. this.selected = true;
  9187. };
  9188. Edge.prototype.unselect = function() {
  9189. this.selected = false;
  9190. };
  9191. Edge.prototype.positionBezierNode = function() {
  9192. if (this.via !== null) {
  9193. this.via.x = 0.5 * (this.from.x + this.to.x);
  9194. this.via.y = 0.5 * (this.from.y + this.to.y);
  9195. }
  9196. };
  9197. /**
  9198. * Popup is a class to create a popup window with some text
  9199. * @param {Element} container The container object.
  9200. * @param {Number} [x]
  9201. * @param {Number} [y]
  9202. * @param {String} [text]
  9203. * @param {Object} [style] An object containing borderColor,
  9204. * backgroundColor, etc.
  9205. */
  9206. function Popup(container, x, y, text, style) {
  9207. if (container) {
  9208. this.container = container;
  9209. }
  9210. else {
  9211. this.container = document.body;
  9212. }
  9213. // x, y and text are optional, see if a style object was passed in their place
  9214. if (style === undefined) {
  9215. if (typeof x === "object") {
  9216. style = x;
  9217. x = undefined;
  9218. } else if (typeof text === "object") {
  9219. style = text;
  9220. text = undefined;
  9221. } else {
  9222. // for backwards compatibility, in case clients other than Graph are creating Popup directly
  9223. style = {
  9224. fontColor: 'black',
  9225. fontSize: 14, // px
  9226. fontFace: 'verdana',
  9227. color: {
  9228. border: '#666',
  9229. background: '#FFFFC6'
  9230. }
  9231. }
  9232. }
  9233. }
  9234. this.x = 0;
  9235. this.y = 0;
  9236. this.padding = 5;
  9237. if (x !== undefined && y !== undefined ) {
  9238. this.setPosition(x, y);
  9239. }
  9240. if (text !== undefined) {
  9241. this.setText(text);
  9242. }
  9243. // create the frame
  9244. this.frame = document.createElement("div");
  9245. var styleAttr = this.frame.style;
  9246. styleAttr.position = "absolute";
  9247. styleAttr.visibility = "hidden";
  9248. styleAttr.border = "1px solid " + style.color.border;
  9249. styleAttr.color = style.fontColor;
  9250. styleAttr.fontSize = style.fontSize + "px";
  9251. styleAttr.fontFamily = style.fontFace;
  9252. styleAttr.padding = this.padding + "px";
  9253. styleAttr.backgroundColor = style.color.background;
  9254. styleAttr.borderRadius = "3px";
  9255. styleAttr.MozBorderRadius = "3px";
  9256. styleAttr.WebkitBorderRadius = "3px";
  9257. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9258. styleAttr.whiteSpace = "nowrap";
  9259. this.container.appendChild(this.frame);
  9260. }
  9261. /**
  9262. * @param {number} x Horizontal position of the popup window
  9263. * @param {number} y Vertical position of the popup window
  9264. */
  9265. Popup.prototype.setPosition = function(x, y) {
  9266. this.x = parseInt(x);
  9267. this.y = parseInt(y);
  9268. };
  9269. /**
  9270. * Set the text for the popup window. This can be HTML code
  9271. * @param {string} text
  9272. */
  9273. Popup.prototype.setText = function(text) {
  9274. this.frame.innerHTML = text;
  9275. };
  9276. /**
  9277. * Show the popup window
  9278. * @param {boolean} show Optional. Show or hide the window
  9279. */
  9280. Popup.prototype.show = function (show) {
  9281. if (show === undefined) {
  9282. show = true;
  9283. }
  9284. if (show) {
  9285. var height = this.frame.clientHeight;
  9286. var width = this.frame.clientWidth;
  9287. var maxHeight = this.frame.parentNode.clientHeight;
  9288. var maxWidth = this.frame.parentNode.clientWidth;
  9289. var top = (this.y - height);
  9290. if (top + height + this.padding > maxHeight) {
  9291. top = maxHeight - height - this.padding;
  9292. }
  9293. if (top < this.padding) {
  9294. top = this.padding;
  9295. }
  9296. var left = this.x;
  9297. if (left + width + this.padding > maxWidth) {
  9298. left = maxWidth - width - this.padding;
  9299. }
  9300. if (left < this.padding) {
  9301. left = this.padding;
  9302. }
  9303. this.frame.style.left = left + "px";
  9304. this.frame.style.top = top + "px";
  9305. this.frame.style.visibility = "visible";
  9306. }
  9307. else {
  9308. this.hide();
  9309. }
  9310. };
  9311. /**
  9312. * Hide the popup window
  9313. */
  9314. Popup.prototype.hide = function () {
  9315. this.frame.style.visibility = "hidden";
  9316. };
  9317. /**
  9318. * @class Groups
  9319. * This class can store groups and properties specific for groups.
  9320. */
  9321. Groups = function () {
  9322. this.clear();
  9323. this.defaultIndex = 0;
  9324. };
  9325. /**
  9326. * default constants for group colors
  9327. */
  9328. Groups.DEFAULT = [
  9329. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9330. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9331. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9332. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9333. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9334. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9335. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9336. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9337. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9338. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9339. ];
  9340. /**
  9341. * Clear all groups
  9342. */
  9343. Groups.prototype.clear = function () {
  9344. this.groups = {};
  9345. this.groups.length = function()
  9346. {
  9347. var i = 0;
  9348. for ( var p in this ) {
  9349. if (this.hasOwnProperty(p)) {
  9350. i++;
  9351. }
  9352. }
  9353. return i;
  9354. }
  9355. };
  9356. /**
  9357. * get group properties of a groupname. If groupname is not found, a new group
  9358. * is added.
  9359. * @param {*} groupname Can be a number, string, Date, etc.
  9360. * @return {Object} group The created group, containing all group properties
  9361. */
  9362. Groups.prototype.get = function (groupname) {
  9363. var group = this.groups[groupname];
  9364. if (group == undefined) {
  9365. // create new group
  9366. var index = this.defaultIndex % Groups.DEFAULT.length;
  9367. this.defaultIndex++;
  9368. group = {};
  9369. group.color = Groups.DEFAULT[index];
  9370. this.groups[groupname] = group;
  9371. }
  9372. return group;
  9373. };
  9374. /**
  9375. * Add a custom group style
  9376. * @param {String} groupname
  9377. * @param {Object} style An object containing borderColor,
  9378. * backgroundColor, etc.
  9379. * @return {Object} group The created group object
  9380. */
  9381. Groups.prototype.add = function (groupname, style) {
  9382. this.groups[groupname] = style;
  9383. if (style.color) {
  9384. style.color = util.parseColor(style.color);
  9385. }
  9386. return style;
  9387. };
  9388. /**
  9389. * @class Images
  9390. * This class loads images and keeps them stored.
  9391. */
  9392. Images = function () {
  9393. this.images = {};
  9394. this.callback = undefined;
  9395. };
  9396. /**
  9397. * Set an onload callback function. This will be called each time an image
  9398. * is loaded
  9399. * @param {function} callback
  9400. */
  9401. Images.prototype.setOnloadCallback = function(callback) {
  9402. this.callback = callback;
  9403. };
  9404. /**
  9405. *
  9406. * @param {string} url Url of the image
  9407. * @return {Image} img The image object
  9408. */
  9409. Images.prototype.load = function(url) {
  9410. var img = this.images[url];
  9411. if (img == undefined) {
  9412. // create the image
  9413. var images = this;
  9414. img = new Image();
  9415. this.images[url] = img;
  9416. img.onload = function() {
  9417. if (images.callback) {
  9418. images.callback(this);
  9419. }
  9420. };
  9421. img.src = url;
  9422. }
  9423. return img;
  9424. };
  9425. /**
  9426. * Created by Alex on 2/6/14.
  9427. */
  9428. var physicsMixin = {
  9429. /**
  9430. * Toggling barnes Hut calculation on and off.
  9431. *
  9432. * @private
  9433. */
  9434. _toggleBarnesHut: function () {
  9435. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  9436. this._loadSelectedForceSolver();
  9437. this.moving = true;
  9438. this.start();
  9439. },
  9440. /**
  9441. * This loads the node force solver based on the barnes hut or repulsion algorithm
  9442. *
  9443. * @private
  9444. */
  9445. _loadSelectedForceSolver: function () {
  9446. // this overloads the this._calculateNodeForces
  9447. if (this.constants.physics.barnesHut.enabled == true) {
  9448. this._clearMixin(repulsionMixin);
  9449. this._clearMixin(hierarchalRepulsionMixin);
  9450. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  9451. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  9452. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  9453. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  9454. this._loadMixin(barnesHutMixin);
  9455. }
  9456. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  9457. this._clearMixin(barnesHutMixin);
  9458. this._clearMixin(repulsionMixin);
  9459. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  9460. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  9461. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  9462. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  9463. this._loadMixin(hierarchalRepulsionMixin);
  9464. }
  9465. else {
  9466. this._clearMixin(barnesHutMixin);
  9467. this._clearMixin(hierarchalRepulsionMixin);
  9468. this.barnesHutTree = undefined;
  9469. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  9470. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  9471. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  9472. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  9473. this._loadMixin(repulsionMixin);
  9474. }
  9475. },
  9476. /**
  9477. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  9478. * if there is more than one node. If it is just one node, we dont calculate anything.
  9479. *
  9480. * @private
  9481. */
  9482. _initializeForceCalculation: function () {
  9483. // stop calculation if there is only one node
  9484. if (this.nodeIndices.length == 1) {
  9485. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  9486. }
  9487. else {
  9488. // if there are too many nodes on screen, we cluster without repositioning
  9489. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  9490. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  9491. }
  9492. // we now start the force calculation
  9493. this._calculateForces();
  9494. }
  9495. },
  9496. /**
  9497. * Calculate the external forces acting on the nodes
  9498. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9499. * @private
  9500. */
  9501. _calculateForces: function () {
  9502. // Gravity is required to keep separated groups from floating off
  9503. // the forces are reset to zero in this loop by using _setForce instead
  9504. // of _addForce
  9505. this._calculateGravitationalForces();
  9506. this._calculateNodeForces();
  9507. if (this.constants.smoothCurves == true) {
  9508. this._calculateSpringForcesWithSupport();
  9509. }
  9510. else {
  9511. this._calculateSpringForces();
  9512. }
  9513. },
  9514. /**
  9515. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  9516. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  9517. * This function joins the datanodes and invisible (called support) nodes into one object.
  9518. * We do this so we do not contaminate this.nodes with the support nodes.
  9519. *
  9520. * @private
  9521. */
  9522. _updateCalculationNodes: function () {
  9523. if (this.constants.smoothCurves == true) {
  9524. this.calculationNodes = {};
  9525. this.calculationNodeIndices = [];
  9526. for (var nodeId in this.nodes) {
  9527. if (this.nodes.hasOwnProperty(nodeId)) {
  9528. this.calculationNodes[nodeId] = this.nodes[nodeId];
  9529. }
  9530. }
  9531. var supportNodes = this.sectors['support']['nodes'];
  9532. for (var supportNodeId in supportNodes) {
  9533. if (supportNodes.hasOwnProperty(supportNodeId)) {
  9534. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  9535. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  9536. }
  9537. else {
  9538. supportNodes[supportNodeId]._setForce(0, 0);
  9539. }
  9540. }
  9541. }
  9542. for (var idx in this.calculationNodes) {
  9543. if (this.calculationNodes.hasOwnProperty(idx)) {
  9544. this.calculationNodeIndices.push(idx);
  9545. }
  9546. }
  9547. }
  9548. else {
  9549. this.calculationNodes = this.nodes;
  9550. this.calculationNodeIndices = this.nodeIndices;
  9551. }
  9552. },
  9553. /**
  9554. * this function applies the central gravity effect to keep groups from floating off
  9555. *
  9556. * @private
  9557. */
  9558. _calculateGravitationalForces: function () {
  9559. var dx, dy, distance, node, i;
  9560. var nodes = this.calculationNodes;
  9561. var gravity = this.constants.physics.centralGravity;
  9562. var gravityForce = 0;
  9563. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  9564. node = nodes[this.calculationNodeIndices[i]];
  9565. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  9566. // gravity does not apply when we are in a pocket sector
  9567. if (this._sector() == "default" && gravity != 0) {
  9568. dx = -node.x;
  9569. dy = -node.y;
  9570. distance = Math.sqrt(dx * dx + dy * dy);
  9571. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  9572. node.fx = dx * gravityForce;
  9573. node.fy = dy * gravityForce;
  9574. }
  9575. else {
  9576. node.fx = 0;
  9577. node.fy = 0;
  9578. }
  9579. }
  9580. },
  9581. /**
  9582. * this function calculates the effects of the springs in the case of unsmooth curves.
  9583. *
  9584. * @private
  9585. */
  9586. _calculateSpringForces: function () {
  9587. var edgeLength, edge, edgeId;
  9588. var dx, dy, fx, fy, springForce, length;
  9589. var edges = this.edges;
  9590. // forces caused by the edges, modelled as springs
  9591. for (edgeId in edges) {
  9592. if (edges.hasOwnProperty(edgeId)) {
  9593. edge = edges[edgeId];
  9594. if (edge.connected) {
  9595. // only calculate forces if nodes are in the same sector
  9596. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9597. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9598. // this implies that the edges between big clusters are longer
  9599. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  9600. dx = (edge.from.x - edge.to.x);
  9601. dy = (edge.from.y - edge.to.y);
  9602. length = Math.sqrt(dx * dx + dy * dy);
  9603. if (length == 0) {
  9604. length = 0.01;
  9605. }
  9606. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  9607. fx = dx * springForce;
  9608. fy = dy * springForce;
  9609. edge.from.fx += fx;
  9610. edge.from.fy += fy;
  9611. edge.to.fx -= fx;
  9612. edge.to.fy -= fy;
  9613. }
  9614. }
  9615. }
  9616. }
  9617. },
  9618. /**
  9619. * This function calculates the springforces on the nodes, accounting for the support nodes.
  9620. *
  9621. * @private
  9622. */
  9623. _calculateSpringForcesWithSupport: function () {
  9624. var edgeLength, edge, edgeId, combinedClusterSize;
  9625. var edges = this.edges;
  9626. // forces caused by the edges, modelled as springs
  9627. for (edgeId in edges) {
  9628. if (edges.hasOwnProperty(edgeId)) {
  9629. edge = edges[edgeId];
  9630. if (edge.connected) {
  9631. // only calculate forces if nodes are in the same sector
  9632. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9633. if (edge.via != null) {
  9634. var node1 = edge.to;
  9635. var node2 = edge.via;
  9636. var node3 = edge.from;
  9637. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9638. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  9639. // this implies that the edges between big clusters are longer
  9640. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  9641. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  9642. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  9643. }
  9644. }
  9645. }
  9646. }
  9647. }
  9648. },
  9649. /**
  9650. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  9651. *
  9652. * @param node1
  9653. * @param node2
  9654. * @param edgeLength
  9655. * @private
  9656. */
  9657. _calculateSpringForce: function (node1, node2, edgeLength) {
  9658. var dx, dy, fx, fy, springForce, length;
  9659. dx = (node1.x - node2.x);
  9660. dy = (node1.y - node2.y);
  9661. length = Math.sqrt(dx * dx + dy * dy);
  9662. if (length == 0) {
  9663. length = 0.01;
  9664. }
  9665. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  9666. fx = dx * springForce;
  9667. fy = dy * springForce;
  9668. node1.fx += fx;
  9669. node1.fy += fy;
  9670. node2.fx -= fx;
  9671. node2.fy -= fy;
  9672. },
  9673. /**
  9674. * Load the HTML for the physics config and bind it
  9675. * @private
  9676. */
  9677. _loadPhysicsConfiguration: function () {
  9678. if (this.physicsConfiguration === undefined) {
  9679. this.backupConstants = {};
  9680. util.copyObject(this.constants, this.backupConstants);
  9681. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  9682. this.physicsConfiguration = document.createElement('div');
  9683. this.physicsConfiguration.className = "PhysicsConfiguration";
  9684. this.physicsConfiguration.innerHTML = '' +
  9685. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  9686. '<tr>' +
  9687. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  9688. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  9689. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  9690. '</tr>' +
  9691. '</table>' +
  9692. '<table id="graph_BH_table" style="display:none">' +
  9693. '<tr><td><b>Barnes Hut</b></td></tr>' +
  9694. '<tr>' +
  9695. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="500" 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>' +
  9696. '</tr>' +
  9697. '<tr>' +
  9698. '<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>' +
  9699. '</tr>' +
  9700. '<tr>' +
  9701. '<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>' +
  9702. '</tr>' +
  9703. '<tr>' +
  9704. '<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>' +
  9705. '</tr>' +
  9706. '<tr>' +
  9707. '<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>' +
  9708. '</tr>' +
  9709. '</table>' +
  9710. '<table id="graph_R_table" style="display:none">' +
  9711. '<tr><td><b>Repulsion</b></td></tr>' +
  9712. '<tr>' +
  9713. '<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>' +
  9714. '</tr>' +
  9715. '<tr>' +
  9716. '<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>' +
  9717. '</tr>' +
  9718. '<tr>' +
  9719. '<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>' +
  9720. '</tr>' +
  9721. '<tr>' +
  9722. '<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>' +
  9723. '</tr>' +
  9724. '<tr>' +
  9725. '<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>' +
  9726. '</tr>' +
  9727. '</table>' +
  9728. '<table id="graph_H_table" style="display:none">' +
  9729. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  9730. '<tr>' +
  9731. '<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>' +
  9732. '</tr>' +
  9733. '<tr>' +
  9734. '<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>' +
  9735. '</tr>' +
  9736. '<tr>' +
  9737. '<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>' +
  9738. '</tr>' +
  9739. '<tr>' +
  9740. '<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>' +
  9741. '</tr>' +
  9742. '<tr>' +
  9743. '<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>' +
  9744. '</tr>' +
  9745. '<tr>' +
  9746. '<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>' +
  9747. '</tr>' +
  9748. '<tr>' +
  9749. '<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>' +
  9750. '</tr>' +
  9751. '<tr>' +
  9752. '<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>' +
  9753. '</tr>' +
  9754. '</table>' +
  9755. '<table><tr><td><b>Options:</b></td></tr>' +
  9756. '<tr>' +
  9757. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  9758. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  9759. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  9760. '</tr>' +
  9761. '</table>'
  9762. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  9763. this.optionsDiv = document.createElement("div");
  9764. this.optionsDiv.style.fontSize = "14px";
  9765. this.optionsDiv.style.fontFamily = "verdana";
  9766. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  9767. var rangeElement;
  9768. rangeElement = document.getElementById('graph_BH_gc');
  9769. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  9770. rangeElement = document.getElementById('graph_BH_cg');
  9771. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  9772. rangeElement = document.getElementById('graph_BH_sc');
  9773. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  9774. rangeElement = document.getElementById('graph_BH_sl');
  9775. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  9776. rangeElement = document.getElementById('graph_BH_damp');
  9777. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  9778. rangeElement = document.getElementById('graph_R_nd');
  9779. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  9780. rangeElement = document.getElementById('graph_R_cg');
  9781. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  9782. rangeElement = document.getElementById('graph_R_sc');
  9783. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  9784. rangeElement = document.getElementById('graph_R_sl');
  9785. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  9786. rangeElement = document.getElementById('graph_R_damp');
  9787. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  9788. rangeElement = document.getElementById('graph_H_nd');
  9789. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  9790. rangeElement = document.getElementById('graph_H_cg');
  9791. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  9792. rangeElement = document.getElementById('graph_H_sc');
  9793. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  9794. rangeElement = document.getElementById('graph_H_sl');
  9795. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  9796. rangeElement = document.getElementById('graph_H_damp');
  9797. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  9798. rangeElement = document.getElementById('graph_H_direction');
  9799. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  9800. rangeElement = document.getElementById('graph_H_levsep');
  9801. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  9802. rangeElement = document.getElementById('graph_H_nspac');
  9803. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  9804. var radioButton1 = document.getElementById("graph_physicsMethod1");
  9805. var radioButton2 = document.getElementById("graph_physicsMethod2");
  9806. var radioButton3 = document.getElementById("graph_physicsMethod3");
  9807. radioButton2.checked = true;
  9808. if (this.constants.physics.barnesHut.enabled) {
  9809. radioButton1.checked = true;
  9810. }
  9811. if (this.constants.hierarchicalLayout.enabled) {
  9812. radioButton3.checked = true;
  9813. }
  9814. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9815. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  9816. var graph_generateOptions = document.getElementById("graph_generateOptions");
  9817. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  9818. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  9819. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  9820. if (this.constants.smoothCurves == true) {
  9821. graph_toggleSmooth.style.background = "#A4FF56";
  9822. }
  9823. else {
  9824. graph_toggleSmooth.style.background = "#FF8532";
  9825. }
  9826. switchConfigurations.apply(this);
  9827. radioButton1.onchange = switchConfigurations.bind(this);
  9828. radioButton2.onchange = switchConfigurations.bind(this);
  9829. radioButton3.onchange = switchConfigurations.bind(this);
  9830. }
  9831. },
  9832. _overWriteGraphConstants: function (constantsVariableName, value) {
  9833. var nameArray = constantsVariableName.split("_");
  9834. if (nameArray.length == 1) {
  9835. this.constants[nameArray[0]] = value;
  9836. }
  9837. else if (nameArray.length == 2) {
  9838. this.constants[nameArray[0]][nameArray[1]] = value;
  9839. }
  9840. else if (nameArray.length == 3) {
  9841. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  9842. }
  9843. }
  9844. };
  9845. function graphToggleSmoothCurves () {
  9846. this.constants.smoothCurves = !this.constants.smoothCurves;
  9847. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9848. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  9849. else {graph_toggleSmooth.style.background = "#FF8532";}
  9850. this._configureSmoothCurves(false);
  9851. };
  9852. function graphRepositionNodes () {
  9853. for (var nodeId in this.calculationNodes) {
  9854. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  9855. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  9856. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  9857. }
  9858. }
  9859. if (this.constants.hierarchicalLayout.enabled == true) {
  9860. this._setupHierarchicalLayout();
  9861. }
  9862. else {
  9863. this.repositionNodes();
  9864. }
  9865. this.moving = true;
  9866. this.start();
  9867. };
  9868. function graphGenerateOptions () {
  9869. var options = "No options are required, default values used.";
  9870. var optionsSpecific = [];
  9871. var radioButton1 = document.getElementById("graph_physicsMethod1");
  9872. var radioButton2 = document.getElementById("graph_physicsMethod2");
  9873. if (radioButton1.checked == true) {
  9874. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  9875. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  9876. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  9877. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  9878. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  9879. if (optionsSpecific.length != 0) {
  9880. options = "var options = {";
  9881. options += "physics: {barnesHut: {";
  9882. for (var i = 0; i < optionsSpecific.length; i++) {
  9883. options += optionsSpecific[i];
  9884. if (i < optionsSpecific.length - 1) {
  9885. options += ", "
  9886. }
  9887. }
  9888. options += '}}'
  9889. }
  9890. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  9891. if (optionsSpecific.length == 0) {options = "var options = {";}
  9892. else {options += ", "}
  9893. options += "smoothCurves: " + this.constants.smoothCurves;
  9894. }
  9895. if (options != "No options are required, default values used.") {
  9896. options += '};'
  9897. }
  9898. }
  9899. else if (radioButton2.checked == true) {
  9900. options = "var options = {";
  9901. options += "physics: {barnesHut: {enabled: false}";
  9902. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  9903. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  9904. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  9905. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  9906. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  9907. if (optionsSpecific.length != 0) {
  9908. options += ", repulsion: {";
  9909. for (var i = 0; i < optionsSpecific.length; i++) {
  9910. options += optionsSpecific[i];
  9911. if (i < optionsSpecific.length - 1) {
  9912. options += ", "
  9913. }
  9914. }
  9915. options += '}}'
  9916. }
  9917. if (optionsSpecific.length == 0) {options += "}"}
  9918. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  9919. options += ", smoothCurves: " + this.constants.smoothCurves;
  9920. }
  9921. options += '};'
  9922. }
  9923. else {
  9924. options = "var options = {";
  9925. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  9926. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  9927. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  9928. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  9929. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  9930. if (optionsSpecific.length != 0) {
  9931. options += "physics: {hierarchicalRepulsion: {";
  9932. for (var i = 0; i < optionsSpecific.length; i++) {
  9933. options += optionsSpecific[i];
  9934. if (i < optionsSpecific.length - 1) {
  9935. options += ", ";
  9936. }
  9937. }
  9938. options += '}},';
  9939. }
  9940. options += 'hierarchicalLayout: {';
  9941. optionsSpecific = [];
  9942. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  9943. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  9944. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  9945. if (optionsSpecific.length != 0) {
  9946. for (var i = 0; i < optionsSpecific.length; i++) {
  9947. options += optionsSpecific[i];
  9948. if (i < optionsSpecific.length - 1) {
  9949. options += ", "
  9950. }
  9951. }
  9952. options += '}'
  9953. }
  9954. else {
  9955. options += "enabled:true}";
  9956. }
  9957. options += '};'
  9958. }
  9959. this.optionsDiv.innerHTML = options;
  9960. };
  9961. function switchConfigurations () {
  9962. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  9963. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  9964. var tableId = "graph_" + radioButton + "_table";
  9965. var table = document.getElementById(tableId);
  9966. table.style.display = "block";
  9967. for (var i = 0; i < ids.length; i++) {
  9968. if (ids[i] != tableId) {
  9969. table = document.getElementById(ids[i]);
  9970. table.style.display = "none";
  9971. }
  9972. }
  9973. this._restoreNodes();
  9974. if (radioButton == "R") {
  9975. this.constants.hierarchicalLayout.enabled = false;
  9976. this.constants.physics.hierarchicalRepulsion.enabled = false;
  9977. this.constants.physics.barnesHut.enabled = false;
  9978. }
  9979. else if (radioButton == "H") {
  9980. this.constants.hierarchicalLayout.enabled = true;
  9981. this.constants.physics.hierarchicalRepulsion.enabled = true;
  9982. this.constants.physics.barnesHut.enabled = false;
  9983. this._setupHierarchicalLayout();
  9984. }
  9985. else {
  9986. this.constants.hierarchicalLayout.enabled = false;
  9987. this.constants.physics.hierarchicalRepulsion.enabled = false;
  9988. this.constants.physics.barnesHut.enabled = true;
  9989. }
  9990. this._loadSelectedForceSolver();
  9991. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9992. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  9993. else {graph_toggleSmooth.style.background = "#FF8532";}
  9994. this.moving = true;
  9995. this.start();
  9996. }
  9997. function showValueOfRange (id,map,constantsVariableName) {
  9998. var valueId = id + "_value";
  9999. var rangeValue = document.getElementById(id).value;
  10000. if (map instanceof Array) {
  10001. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  10002. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  10003. }
  10004. else {
  10005. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  10006. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  10007. }
  10008. if (constantsVariableName == "hierarchicalLayout_direction" ||
  10009. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  10010. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  10011. this._setupHierarchicalLayout();
  10012. }
  10013. this.moving = true;
  10014. this.start();
  10015. };
  10016. /**
  10017. * Created by Alex on 2/10/14.
  10018. */
  10019. var hierarchalRepulsionMixin = {
  10020. /**
  10021. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10022. * This field is linearly approximated.
  10023. *
  10024. * @private
  10025. */
  10026. _calculateNodeForces: function () {
  10027. var dx, dy, distance, fx, fy, combinedClusterSize,
  10028. repulsingForce, node1, node2, i, j;
  10029. var nodes = this.calculationNodes;
  10030. var nodeIndices = this.calculationNodeIndices;
  10031. // approximation constants
  10032. var b = 5;
  10033. var a_base = 0.5 * -b;
  10034. // repulsing forces between nodes
  10035. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10036. var minimumDistance = nodeDistance;
  10037. // we loop from i over all but the last entree in the array
  10038. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10039. for (i = 0; i < nodeIndices.length - 1; i++) {
  10040. node1 = nodes[nodeIndices[i]];
  10041. for (j = i + 1; j < nodeIndices.length; j++) {
  10042. node2 = nodes[nodeIndices[j]];
  10043. dx = node2.x - node1.x;
  10044. dy = node2.y - node1.y;
  10045. distance = Math.sqrt(dx * dx + dy * dy);
  10046. var a = a_base / minimumDistance;
  10047. if (distance < 2 * minimumDistance) {
  10048. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10049. // normalize force with
  10050. if (distance == 0) {
  10051. distance = 0.01;
  10052. }
  10053. else {
  10054. repulsingForce = repulsingForce / distance;
  10055. }
  10056. fx = dx * repulsingForce;
  10057. fy = dy * repulsingForce;
  10058. node1.fx -= fx;
  10059. node1.fy -= fy;
  10060. node2.fx += fx;
  10061. node2.fy += fy;
  10062. }
  10063. }
  10064. }
  10065. }
  10066. };
  10067. /**
  10068. * Created by Alex on 2/10/14.
  10069. */
  10070. var barnesHutMixin = {
  10071. /**
  10072. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10073. * The Barnes Hut method is used to speed up this N-body simulation.
  10074. *
  10075. * @private
  10076. */
  10077. _calculateNodeForces : function() {
  10078. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  10079. var node;
  10080. var nodes = this.calculationNodes;
  10081. var nodeIndices = this.calculationNodeIndices;
  10082. var nodeCount = nodeIndices.length;
  10083. this._formBarnesHutTree(nodes,nodeIndices);
  10084. var barnesHutTree = this.barnesHutTree;
  10085. // place the nodes one by one recursively
  10086. for (var i = 0; i < nodeCount; i++) {
  10087. node = nodes[nodeIndices[i]];
  10088. // starting with root is irrelevant, it never passes the BarnesHut condition
  10089. this._getForceContribution(barnesHutTree.root.children.NW,node);
  10090. this._getForceContribution(barnesHutTree.root.children.NE,node);
  10091. this._getForceContribution(barnesHutTree.root.children.SW,node);
  10092. this._getForceContribution(barnesHutTree.root.children.SE,node);
  10093. }
  10094. }
  10095. },
  10096. /**
  10097. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  10098. * If a region contains a single node, we check if it is not itself, then we apply the force.
  10099. *
  10100. * @param parentBranch
  10101. * @param node
  10102. * @private
  10103. */
  10104. _getForceContribution : function(parentBranch,node) {
  10105. // we get no force contribution from an empty region
  10106. if (parentBranch.childrenCount > 0) {
  10107. var dx,dy,distance;
  10108. // get the distance from the center of mass to the node.
  10109. dx = parentBranch.centerOfMass.x - node.x;
  10110. dy = parentBranch.centerOfMass.y - node.y;
  10111. distance = Math.sqrt(dx * dx + dy * dy);
  10112. // BarnesHut condition
  10113. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  10114. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  10115. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  10116. // duplicate code to reduce function calls to speed up program
  10117. if (distance == 0) {
  10118. distance = 0.1*Math.random();
  10119. dx = distance;
  10120. }
  10121. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10122. var fx = dx * gravityForce;
  10123. var fy = dy * gravityForce;
  10124. node.fx += fx;
  10125. node.fy += fy;
  10126. }
  10127. else {
  10128. // Did not pass the condition, go into children if available
  10129. if (parentBranch.childrenCount == 4) {
  10130. this._getForceContribution(parentBranch.children.NW,node);
  10131. this._getForceContribution(parentBranch.children.NE,node);
  10132. this._getForceContribution(parentBranch.children.SW,node);
  10133. this._getForceContribution(parentBranch.children.SE,node);
  10134. }
  10135. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  10136. if (parentBranch.children.data.id != node.id) { // if it is not self
  10137. // duplicate code to reduce function calls to speed up program
  10138. if (distance == 0) {
  10139. distance = 0.5*Math.random();
  10140. dx = distance;
  10141. }
  10142. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10143. var fx = dx * gravityForce;
  10144. var fy = dy * gravityForce;
  10145. node.fx += fx;
  10146. node.fy += fy;
  10147. }
  10148. }
  10149. }
  10150. }
  10151. },
  10152. /**
  10153. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  10154. *
  10155. * @param nodes
  10156. * @param nodeIndices
  10157. * @private
  10158. */
  10159. _formBarnesHutTree : function(nodes,nodeIndices) {
  10160. var node;
  10161. var nodeCount = nodeIndices.length;
  10162. var minX = Number.MAX_VALUE,
  10163. minY = Number.MAX_VALUE,
  10164. maxX =-Number.MAX_VALUE,
  10165. maxY =-Number.MAX_VALUE;
  10166. // get the range of the nodes
  10167. for (var i = 0; i < nodeCount; i++) {
  10168. var x = nodes[nodeIndices[i]].x;
  10169. var y = nodes[nodeIndices[i]].y;
  10170. if (x < minX) { minX = x; }
  10171. if (x > maxX) { maxX = x; }
  10172. if (y < minY) { minY = y; }
  10173. if (y > maxY) { maxY = y; }
  10174. }
  10175. // make the range a square
  10176. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  10177. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  10178. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  10179. var minimumTreeSize = 1e-5;
  10180. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  10181. var halfRootSize = 0.5 * rootSize;
  10182. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  10183. // construct the barnesHutTree
  10184. var barnesHutTree = {root:{
  10185. centerOfMass:{x:0,y:0}, // Center of Mass
  10186. mass:0,
  10187. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  10188. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  10189. size: rootSize,
  10190. calcSize: 1 / rootSize,
  10191. children: {data:null},
  10192. maxWidth: 0,
  10193. level: 0,
  10194. childrenCount: 4
  10195. }};
  10196. this._splitBranch(barnesHutTree.root);
  10197. // place the nodes one by one recursively
  10198. for (i = 0; i < nodeCount; i++) {
  10199. node = nodes[nodeIndices[i]];
  10200. this._placeInTree(barnesHutTree.root,node);
  10201. }
  10202. // make global
  10203. this.barnesHutTree = barnesHutTree
  10204. },
  10205. _updateBranchMass : function(parentBranch, node) {
  10206. var totalMass = parentBranch.mass + node.mass;
  10207. var totalMassInv = 1/totalMass;
  10208. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  10209. parentBranch.centerOfMass.x *= totalMassInv;
  10210. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  10211. parentBranch.centerOfMass.y *= totalMassInv;
  10212. parentBranch.mass = totalMass;
  10213. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  10214. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  10215. },
  10216. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  10217. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  10218. // update the mass of the branch.
  10219. this._updateBranchMass(parentBranch,node);
  10220. }
  10221. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  10222. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  10223. this._placeInRegion(parentBranch,node,"NW");
  10224. }
  10225. else { // in SW
  10226. this._placeInRegion(parentBranch,node,"SW");
  10227. }
  10228. }
  10229. else { // in NE or SE
  10230. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  10231. this._placeInRegion(parentBranch,node,"NE");
  10232. }
  10233. else { // in SE
  10234. this._placeInRegion(parentBranch,node,"SE");
  10235. }
  10236. }
  10237. },
  10238. _placeInRegion : function(parentBranch,node,region) {
  10239. switch (parentBranch.children[region].childrenCount) {
  10240. case 0: // place node here
  10241. parentBranch.children[region].children.data = node;
  10242. parentBranch.children[region].childrenCount = 1;
  10243. this._updateBranchMass(parentBranch.children[region],node);
  10244. break;
  10245. case 1: // convert into children
  10246. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  10247. // we move one node a pixel and we do not put it in the tree.
  10248. if (parentBranch.children[region].children.data.x == node.x &&
  10249. parentBranch.children[region].children.data.y == node.y) {
  10250. node.x += Math.random();
  10251. node.y += Math.random();
  10252. }
  10253. else {
  10254. this._splitBranch(parentBranch.children[region]);
  10255. this._placeInTree(parentBranch.children[region],node);
  10256. }
  10257. break;
  10258. case 4: // place in branch
  10259. this._placeInTree(parentBranch.children[region],node);
  10260. break;
  10261. }
  10262. },
  10263. /**
  10264. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  10265. * after the split is complete.
  10266. *
  10267. * @param parentBranch
  10268. * @private
  10269. */
  10270. _splitBranch : function(parentBranch) {
  10271. // if the branch is filled with a node, replace the node in the new subset.
  10272. var containedNode = null;
  10273. if (parentBranch.childrenCount == 1) {
  10274. containedNode = parentBranch.children.data;
  10275. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  10276. }
  10277. parentBranch.childrenCount = 4;
  10278. parentBranch.children.data = null;
  10279. this._insertRegion(parentBranch,"NW");
  10280. this._insertRegion(parentBranch,"NE");
  10281. this._insertRegion(parentBranch,"SW");
  10282. this._insertRegion(parentBranch,"SE");
  10283. if (containedNode != null) {
  10284. this._placeInTree(parentBranch,containedNode);
  10285. }
  10286. },
  10287. /**
  10288. * This function subdivides the region into four new segments.
  10289. * Specifically, this inserts a single new segment.
  10290. * It fills the children section of the parentBranch
  10291. *
  10292. * @param parentBranch
  10293. * @param region
  10294. * @param parentRange
  10295. * @private
  10296. */
  10297. _insertRegion : function(parentBranch, region) {
  10298. var minX,maxX,minY,maxY;
  10299. var childSize = 0.5 * parentBranch.size;
  10300. switch (region) {
  10301. case "NW":
  10302. minX = parentBranch.range.minX;
  10303. maxX = parentBranch.range.minX + childSize;
  10304. minY = parentBranch.range.minY;
  10305. maxY = parentBranch.range.minY + childSize;
  10306. break;
  10307. case "NE":
  10308. minX = parentBranch.range.minX + childSize;
  10309. maxX = parentBranch.range.maxX;
  10310. minY = parentBranch.range.minY;
  10311. maxY = parentBranch.range.minY + childSize;
  10312. break;
  10313. case "SW":
  10314. minX = parentBranch.range.minX;
  10315. maxX = parentBranch.range.minX + childSize;
  10316. minY = parentBranch.range.minY + childSize;
  10317. maxY = parentBranch.range.maxY;
  10318. break;
  10319. case "SE":
  10320. minX = parentBranch.range.minX + childSize;
  10321. maxX = parentBranch.range.maxX;
  10322. minY = parentBranch.range.minY + childSize;
  10323. maxY = parentBranch.range.maxY;
  10324. break;
  10325. }
  10326. parentBranch.children[region] = {
  10327. centerOfMass:{x:0,y:0},
  10328. mass:0,
  10329. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  10330. size: 0.5 * parentBranch.size,
  10331. calcSize: 2 * parentBranch.calcSize,
  10332. children: {data:null},
  10333. maxWidth: 0,
  10334. level: parentBranch.level+1,
  10335. childrenCount: 0
  10336. };
  10337. },
  10338. /**
  10339. * This function is for debugging purposed, it draws the tree.
  10340. *
  10341. * @param ctx
  10342. * @param color
  10343. * @private
  10344. */
  10345. _drawTree : function(ctx,color) {
  10346. if (this.barnesHutTree !== undefined) {
  10347. ctx.lineWidth = 1;
  10348. this._drawBranch(this.barnesHutTree.root,ctx,color);
  10349. }
  10350. },
  10351. /**
  10352. * This function is for debugging purposes. It draws the branches recursively.
  10353. *
  10354. * @param branch
  10355. * @param ctx
  10356. * @param color
  10357. * @private
  10358. */
  10359. _drawBranch : function(branch,ctx,color) {
  10360. if (color === undefined) {
  10361. color = "#FF0000";
  10362. }
  10363. if (branch.childrenCount == 4) {
  10364. this._drawBranch(branch.children.NW,ctx);
  10365. this._drawBranch(branch.children.NE,ctx);
  10366. this._drawBranch(branch.children.SE,ctx);
  10367. this._drawBranch(branch.children.SW,ctx);
  10368. }
  10369. ctx.strokeStyle = color;
  10370. ctx.beginPath();
  10371. ctx.moveTo(branch.range.minX,branch.range.minY);
  10372. ctx.lineTo(branch.range.maxX,branch.range.minY);
  10373. ctx.stroke();
  10374. ctx.beginPath();
  10375. ctx.moveTo(branch.range.maxX,branch.range.minY);
  10376. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  10377. ctx.stroke();
  10378. ctx.beginPath();
  10379. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  10380. ctx.lineTo(branch.range.minX,branch.range.maxY);
  10381. ctx.stroke();
  10382. ctx.beginPath();
  10383. ctx.moveTo(branch.range.minX,branch.range.maxY);
  10384. ctx.lineTo(branch.range.minX,branch.range.minY);
  10385. ctx.stroke();
  10386. /*
  10387. if (branch.mass > 0) {
  10388. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  10389. ctx.stroke();
  10390. }
  10391. */
  10392. }
  10393. };
  10394. /**
  10395. * Created by Alex on 2/10/14.
  10396. */
  10397. var repulsionMixin = {
  10398. /**
  10399. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10400. * This field is linearly approximated.
  10401. *
  10402. * @private
  10403. */
  10404. _calculateNodeForces: function () {
  10405. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  10406. repulsingForce, node1, node2, i, j;
  10407. var nodes = this.calculationNodes;
  10408. var nodeIndices = this.calculationNodeIndices;
  10409. // approximation constants
  10410. var a_base = -2 / 3;
  10411. var b = 4 / 3;
  10412. // repulsing forces between nodes
  10413. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  10414. var minimumDistance = nodeDistance;
  10415. // we loop from i over all but the last entree in the array
  10416. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10417. for (i = 0; i < nodeIndices.length - 1; i++) {
  10418. node1 = nodes[nodeIndices[i]];
  10419. for (j = i + 1; j < nodeIndices.length; j++) {
  10420. node2 = nodes[nodeIndices[j]];
  10421. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  10422. dx = node2.x - node1.x;
  10423. dy = node2.y - node1.y;
  10424. distance = Math.sqrt(dx * dx + dy * dy);
  10425. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  10426. var a = a_base / minimumDistance;
  10427. if (distance < 2 * minimumDistance) {
  10428. if (distance < 0.5 * minimumDistance) {
  10429. repulsingForce = 1.0;
  10430. }
  10431. else {
  10432. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10433. }
  10434. // amplify the repulsion for clusters.
  10435. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  10436. repulsingForce = repulsingForce / distance;
  10437. fx = dx * repulsingForce;
  10438. fy = dy * repulsingForce;
  10439. node1.fx -= fx;
  10440. node1.fy -= fy;
  10441. node2.fx += fx;
  10442. node2.fy += fy;
  10443. }
  10444. }
  10445. }
  10446. }
  10447. };
  10448. var HierarchicalLayoutMixin = {
  10449. _resetLevels : function() {
  10450. for (var nodeId in this.nodes) {
  10451. if (this.nodes.hasOwnProperty(nodeId)) {
  10452. var node = this.nodes[nodeId];
  10453. if (node.preassignedLevel == false) {
  10454. node.level = -1;
  10455. }
  10456. }
  10457. }
  10458. },
  10459. /**
  10460. * This is the main function to layout the nodes in a hierarchical way.
  10461. * It checks if the node details are supplied correctly
  10462. *
  10463. * @private
  10464. */
  10465. _setupHierarchicalLayout : function() {
  10466. if (this.constants.hierarchicalLayout.enabled == true) {
  10467. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  10468. this.constants.hierarchicalLayout.levelSeparation *= -1;
  10469. }
  10470. else {
  10471. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  10472. }
  10473. // get the size of the largest hubs and check if the user has defined a level for a node.
  10474. var hubsize = 0;
  10475. var node, nodeId;
  10476. var definedLevel = false;
  10477. var undefinedLevel = false;
  10478. for (nodeId in this.nodes) {
  10479. if (this.nodes.hasOwnProperty(nodeId)) {
  10480. node = this.nodes[nodeId];
  10481. if (node.level != -1) {
  10482. definedLevel = true;
  10483. }
  10484. else {
  10485. undefinedLevel = true;
  10486. }
  10487. if (hubsize < node.edges.length) {
  10488. hubsize = node.edges.length;
  10489. }
  10490. }
  10491. }
  10492. // if the user defined some levels but not all, alert and run without hierarchical layout
  10493. if (undefinedLevel == true && definedLevel == true) {
  10494. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  10495. this.zoomExtent(true,this.constants.clustering.enabled);
  10496. if (!this.constants.clustering.enabled) {
  10497. this.start();
  10498. }
  10499. }
  10500. else {
  10501. // setup the system to use hierarchical method.
  10502. this._changeConstants();
  10503. // define levels if undefined by the users. Based on hubsize
  10504. if (undefinedLevel == true) {
  10505. this._determineLevels(hubsize);
  10506. }
  10507. // check the distribution of the nodes per level.
  10508. var distribution = this._getDistribution();
  10509. // place the nodes on the canvas. This also stablilizes the system.
  10510. this._placeNodesByHierarchy(distribution);
  10511. // start the simulation.
  10512. this.start();
  10513. }
  10514. }
  10515. },
  10516. /**
  10517. * This function places the nodes on the canvas based on the hierarchial distribution.
  10518. *
  10519. * @param {Object} distribution | obtained by the function this._getDistribution()
  10520. * @private
  10521. */
  10522. _placeNodesByHierarchy : function(distribution) {
  10523. var nodeId, node;
  10524. // start placing all the level 0 nodes first. Then recursively position their branches.
  10525. for (nodeId in distribution[0].nodes) {
  10526. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  10527. node = distribution[0].nodes[nodeId];
  10528. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10529. if (node.xFixed) {
  10530. node.x = distribution[0].minPos;
  10531. node.xFixed = false;
  10532. distribution[0].minPos += distribution[0].nodeSpacing;
  10533. }
  10534. }
  10535. else {
  10536. if (node.yFixed) {
  10537. node.y = distribution[0].minPos;
  10538. node.yFixed = false;
  10539. distribution[0].minPos += distribution[0].nodeSpacing;
  10540. }
  10541. }
  10542. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  10543. }
  10544. }
  10545. // stabilize the system after positioning. This function calls zoomExtent.
  10546. this._stabilize();
  10547. },
  10548. /**
  10549. * This function get the distribution of levels based on hubsize
  10550. *
  10551. * @returns {Object}
  10552. * @private
  10553. */
  10554. _getDistribution : function() {
  10555. var distribution = {};
  10556. var nodeId, node;
  10557. // 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.
  10558. // the fix of X is removed after the x value has been set.
  10559. for (nodeId in this.nodes) {
  10560. if (this.nodes.hasOwnProperty(nodeId)) {
  10561. node = this.nodes[nodeId];
  10562. node.xFixed = true;
  10563. node.yFixed = true;
  10564. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10565. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10566. }
  10567. else {
  10568. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10569. }
  10570. if (!distribution.hasOwnProperty(node.level)) {
  10571. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  10572. }
  10573. distribution[node.level].amount += 1;
  10574. distribution[node.level].nodes[node.id] = node;
  10575. }
  10576. }
  10577. // determine the largest amount of nodes of all levels
  10578. var maxCount = 0;
  10579. for (var level in distribution) {
  10580. if (distribution.hasOwnProperty(level)) {
  10581. if (maxCount < distribution[level].amount) {
  10582. maxCount = distribution[level].amount;
  10583. }
  10584. }
  10585. }
  10586. // set the initial position and spacing of each nodes accordingly
  10587. for (var level in distribution) {
  10588. if (distribution.hasOwnProperty(level)) {
  10589. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  10590. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  10591. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  10592. }
  10593. }
  10594. return distribution;
  10595. },
  10596. /**
  10597. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  10598. *
  10599. * @param hubsize
  10600. * @private
  10601. */
  10602. _determineLevels : function(hubsize) {
  10603. var nodeId, node;
  10604. // determine hubs
  10605. for (nodeId in this.nodes) {
  10606. if (this.nodes.hasOwnProperty(nodeId)) {
  10607. node = this.nodes[nodeId];
  10608. if (node.edges.length == hubsize) {
  10609. node.level = 0;
  10610. }
  10611. }
  10612. }
  10613. // branch from hubs
  10614. for (nodeId in this.nodes) {
  10615. if (this.nodes.hasOwnProperty(nodeId)) {
  10616. node = this.nodes[nodeId];
  10617. if (node.level == 0) {
  10618. this._setLevel(1,node.edges,node.id);
  10619. }
  10620. }
  10621. }
  10622. },
  10623. /**
  10624. * Since hierarchical layout does not support:
  10625. * - smooth curves (based on the physics),
  10626. * - clustering (based on dynamic node counts)
  10627. *
  10628. * We disable both features so there will be no problems.
  10629. *
  10630. * @private
  10631. */
  10632. _changeConstants : function() {
  10633. this.constants.clustering.enabled = false;
  10634. this.constants.physics.barnesHut.enabled = false;
  10635. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10636. this._loadSelectedForceSolver();
  10637. this.constants.smoothCurves = false;
  10638. this._configureSmoothCurves();
  10639. },
  10640. /**
  10641. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  10642. * on a X position that ensures there will be no overlap.
  10643. *
  10644. * @param edges
  10645. * @param parentId
  10646. * @param distribution
  10647. * @param parentLevel
  10648. * @private
  10649. */
  10650. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  10651. for (var i = 0; i < edges.length; i++) {
  10652. var childNode = null;
  10653. if (edges[i].toId == parentId) {
  10654. childNode = edges[i].from;
  10655. }
  10656. else {
  10657. childNode = edges[i].to;
  10658. }
  10659. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  10660. var nodeMoved = false;
  10661. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10662. if (childNode.xFixed && childNode.level > parentLevel) {
  10663. childNode.xFixed = false;
  10664. childNode.x = distribution[childNode.level].minPos;
  10665. nodeMoved = true;
  10666. }
  10667. }
  10668. else {
  10669. if (childNode.yFixed && childNode.level > parentLevel) {
  10670. childNode.yFixed = false;
  10671. childNode.y = distribution[childNode.level].minPos;
  10672. nodeMoved = true;
  10673. }
  10674. }
  10675. if (nodeMoved == true) {
  10676. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  10677. if (childNode.edges.length > 1) {
  10678. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  10679. }
  10680. }
  10681. }
  10682. },
  10683. /**
  10684. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  10685. *
  10686. * @param level
  10687. * @param edges
  10688. * @param parentId
  10689. * @private
  10690. */
  10691. _setLevel : function(level, edges, parentId) {
  10692. for (var i = 0; i < edges.length; i++) {
  10693. var childNode = null;
  10694. if (edges[i].toId == parentId) {
  10695. childNode = edges[i].from;
  10696. }
  10697. else {
  10698. childNode = edges[i].to;
  10699. }
  10700. if (childNode.level == -1 || childNode.level > level) {
  10701. childNode.level = level;
  10702. if (edges.length > 1) {
  10703. this._setLevel(level+1, childNode.edges, childNode.id);
  10704. }
  10705. }
  10706. }
  10707. },
  10708. /**
  10709. * Unfix nodes
  10710. *
  10711. * @private
  10712. */
  10713. _restoreNodes : function() {
  10714. for (nodeId in this.nodes) {
  10715. if (this.nodes.hasOwnProperty(nodeId)) {
  10716. this.nodes[nodeId].xFixed = false;
  10717. this.nodes[nodeId].yFixed = false;
  10718. }
  10719. }
  10720. }
  10721. };
  10722. /**
  10723. * Created by Alex on 2/4/14.
  10724. */
  10725. var manipulationMixin = {
  10726. /**
  10727. * clears the toolbar div element of children
  10728. *
  10729. * @private
  10730. */
  10731. _clearManipulatorBar : function() {
  10732. while (this.manipulationDiv.hasChildNodes()) {
  10733. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  10734. }
  10735. },
  10736. /**
  10737. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  10738. * these functions to their original functionality, we saved them in this.cachedFunctions.
  10739. * This function restores these functions to their original function.
  10740. *
  10741. * @private
  10742. */
  10743. _restoreOverloadedFunctions : function() {
  10744. for (var functionName in this.cachedFunctions) {
  10745. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  10746. this[functionName] = this.cachedFunctions[functionName];
  10747. }
  10748. }
  10749. },
  10750. /**
  10751. * Enable or disable edit-mode.
  10752. *
  10753. * @private
  10754. */
  10755. _toggleEditMode : function() {
  10756. this.editMode = !this.editMode;
  10757. var toolbar = document.getElementById("graph-manipulationDiv");
  10758. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  10759. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  10760. if (this.editMode == true) {
  10761. toolbar.style.display="block";
  10762. closeDiv.style.display="block";
  10763. editModeDiv.style.display="none";
  10764. closeDiv.onclick = this._toggleEditMode.bind(this);
  10765. }
  10766. else {
  10767. toolbar.style.display="none";
  10768. closeDiv.style.display="none";
  10769. editModeDiv.style.display="block";
  10770. closeDiv.onclick = null;
  10771. }
  10772. this._createManipulatorBar()
  10773. },
  10774. /**
  10775. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  10776. *
  10777. * @private
  10778. */
  10779. _createManipulatorBar : function() {
  10780. // remove bound functions
  10781. if (this.boundFunction) {
  10782. this.off('select', this.boundFunction);
  10783. }
  10784. // restore overloaded functions
  10785. this._restoreOverloadedFunctions();
  10786. // resume calculation
  10787. this.freezeSimulation = false;
  10788. // reset global variables
  10789. this.blockConnectingEdgeSelection = false;
  10790. this.forceAppendSelection = false;
  10791. if (this.editMode == true) {
  10792. while (this.manipulationDiv.hasChildNodes()) {
  10793. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  10794. }
  10795. // add the icons to the manipulator div
  10796. this.manipulationDiv.innerHTML = "" +
  10797. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  10798. "<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  10799. "<div class='graph-seperatorLine'></div>" +
  10800. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  10801. "<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  10802. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  10803. this.manipulationDiv.innerHTML += "" +
  10804. "<div class='graph-seperatorLine'></div>" +
  10805. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  10806. "<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  10807. }
  10808. if (this._selectionIsEmpty() == false) {
  10809. this.manipulationDiv.innerHTML += "" +
  10810. "<div class='graph-seperatorLine'></div>" +
  10811. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  10812. "<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  10813. }
  10814. // bind the icons
  10815. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  10816. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  10817. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  10818. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  10819. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  10820. var editButton = document.getElementById("graph-manipulate-editNode");
  10821. editButton.onclick = this._editNode.bind(this);
  10822. }
  10823. if (this._selectionIsEmpty() == false) {
  10824. var deleteButton = document.getElementById("graph-manipulate-delete");
  10825. deleteButton.onclick = this._deleteSelected.bind(this);
  10826. }
  10827. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  10828. closeDiv.onclick = this._toggleEditMode.bind(this);
  10829. this.boundFunction = this._createManipulatorBar.bind(this);
  10830. this.on('select', this.boundFunction);
  10831. }
  10832. else {
  10833. this.editModeDiv.innerHTML = "" +
  10834. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  10835. "<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  10836. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  10837. editModeButton.onclick = this._toggleEditMode.bind(this);
  10838. }
  10839. },
  10840. /**
  10841. * Create the toolbar for adding Nodes
  10842. *
  10843. * @private
  10844. */
  10845. _createAddNodeToolbar : function() {
  10846. // clear the toolbar
  10847. this._clearManipulatorBar();
  10848. if (this.boundFunction) {
  10849. this.off('select', this.boundFunction);
  10850. }
  10851. // create the toolbar contents
  10852. this.manipulationDiv.innerHTML = "" +
  10853. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  10854. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  10855. "<div class='graph-seperatorLine'></div>" +
  10856. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  10857. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  10858. // bind the icon
  10859. var backButton = document.getElementById("graph-manipulate-back");
  10860. backButton.onclick = this._createManipulatorBar.bind(this);
  10861. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  10862. this.boundFunction = this._addNode.bind(this);
  10863. this.on('select', this.boundFunction);
  10864. },
  10865. /**
  10866. * create the toolbar to connect nodes
  10867. *
  10868. * @private
  10869. */
  10870. _createAddEdgeToolbar : function() {
  10871. // clear the toolbar
  10872. this._clearManipulatorBar();
  10873. this._unselectAll(true);
  10874. this.freezeSimulation = true;
  10875. if (this.boundFunction) {
  10876. this.off('select', this.boundFunction);
  10877. }
  10878. this._unselectAll();
  10879. this.forceAppendSelection = false;
  10880. this.blockConnectingEdgeSelection = true;
  10881. this.manipulationDiv.innerHTML = "" +
  10882. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  10883. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  10884. "<div class='graph-seperatorLine'></div>" +
  10885. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  10886. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  10887. // bind the icon
  10888. var backButton = document.getElementById("graph-manipulate-back");
  10889. backButton.onclick = this._createManipulatorBar.bind(this);
  10890. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  10891. this.boundFunction = this._handleConnect.bind(this);
  10892. this.on('select', this.boundFunction);
  10893. // temporarily overload functions
  10894. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  10895. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  10896. this._handleTouch = this._handleConnect;
  10897. this._handleOnRelease = this._finishConnect;
  10898. // redraw to show the unselect
  10899. this._redraw();
  10900. },
  10901. /**
  10902. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  10903. * to walk the user through the process.
  10904. *
  10905. * @private
  10906. */
  10907. _handleConnect : function(pointer) {
  10908. if (this._getSelectedNodeCount() == 0) {
  10909. var node = this._getNodeAt(pointer);
  10910. if (node != null) {
  10911. if (node.clusterSize > 1) {
  10912. alert("Cannot create edges to a cluster.")
  10913. }
  10914. else {
  10915. this._selectObject(node,false);
  10916. // create a node the temporary line can look at
  10917. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  10918. this.sectors['support']['nodes']['targetNode'].x = node.x;
  10919. this.sectors['support']['nodes']['targetNode'].y = node.y;
  10920. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  10921. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  10922. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  10923. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  10924. // create a temporary edge
  10925. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  10926. this.edges['connectionEdge'].from = node;
  10927. this.edges['connectionEdge'].connected = true;
  10928. this.edges['connectionEdge'].smooth = true;
  10929. this.edges['connectionEdge'].selected = true;
  10930. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  10931. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  10932. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  10933. this._handleOnDrag = function(event) {
  10934. var pointer = this._getPointer(event.gesture.center);
  10935. this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
  10936. this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
  10937. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
  10938. this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
  10939. };
  10940. this.moving = true;
  10941. this.start();
  10942. }
  10943. }
  10944. }
  10945. },
  10946. _finishConnect : function(pointer) {
  10947. if (this._getSelectedNodeCount() == 1) {
  10948. // restore the drag function
  10949. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  10950. delete this.cachedFunctions["_handleOnDrag"];
  10951. // remember the edge id
  10952. var connectFromId = this.edges['connectionEdge'].fromId;
  10953. // remove the temporary nodes and edge
  10954. delete this.edges['connectionEdge'];
  10955. delete this.sectors['support']['nodes']['targetNode'];
  10956. delete this.sectors['support']['nodes']['targetViaNode'];
  10957. var node = this._getNodeAt(pointer);
  10958. if (node != null) {
  10959. if (node.clusterSize > 1) {
  10960. alert("Cannot create edges to a cluster.")
  10961. }
  10962. else {
  10963. this._createEdge(connectFromId,node.id);
  10964. this._createManipulatorBar();
  10965. }
  10966. }
  10967. this._unselectAll();
  10968. }
  10969. },
  10970. /**
  10971. * Adds a node on the specified location
  10972. *
  10973. * @param {Object} pointer
  10974. */
  10975. _addNode : function() {
  10976. if (this._selectionIsEmpty() && this.editMode == true) {
  10977. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  10978. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  10979. if (this.triggerFunctions.add) {
  10980. if (this.triggerFunctions.add.length == 2) {
  10981. var me = this;
  10982. this.triggerFunctions.add(defaultData, function(finalizedData) {
  10983. me.nodesData.add(finalizedData);
  10984. me._createManipulatorBar();
  10985. me.moving = true;
  10986. me.start();
  10987. });
  10988. }
  10989. else {
  10990. alert(this.constants.labels['addError']);
  10991. this._createManipulatorBar();
  10992. this.moving = true;
  10993. this.start();
  10994. }
  10995. }
  10996. else {
  10997. this.nodesData.add(defaultData);
  10998. this._createManipulatorBar();
  10999. this.moving = true;
  11000. this.start();
  11001. }
  11002. }
  11003. },
  11004. /**
  11005. * connect two nodes with a new edge.
  11006. *
  11007. * @private
  11008. */
  11009. _createEdge : function(sourceNodeId,targetNodeId) {
  11010. if (this.editMode == true) {
  11011. var defaultData = {from:sourceNodeId, to:targetNodeId};
  11012. if (this.triggerFunctions.connect) {
  11013. if (this.triggerFunctions.connect.length == 2) {
  11014. var me = this;
  11015. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  11016. me.edgesData.add(finalizedData);
  11017. me.moving = true;
  11018. me.start();
  11019. });
  11020. }
  11021. else {
  11022. alert(this.constants.labels["linkError"]);
  11023. this.moving = true;
  11024. this.start();
  11025. }
  11026. }
  11027. else {
  11028. this.edgesData.add(defaultData);
  11029. this.moving = true;
  11030. this.start();
  11031. }
  11032. }
  11033. },
  11034. /**
  11035. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  11036. *
  11037. * @private
  11038. */
  11039. _editNode : function() {
  11040. if (this.triggerFunctions.edit && this.editMode == true) {
  11041. var node = this._getSelectedNode();
  11042. var data = {id:node.id,
  11043. label: node.label,
  11044. group: node.group,
  11045. shape: node.shape,
  11046. color: {
  11047. background:node.color.background,
  11048. border:node.color.border,
  11049. highlight: {
  11050. background:node.color.highlight.background,
  11051. border:node.color.highlight.border
  11052. }
  11053. }};
  11054. if (this.triggerFunctions.edit.length == 2) {
  11055. var me = this;
  11056. this.triggerFunctions.edit(data, function (finalizedData) {
  11057. me.nodesData.update(finalizedData);
  11058. me._createManipulatorBar();
  11059. me.moving = true;
  11060. me.start();
  11061. });
  11062. }
  11063. else {
  11064. alert(this.constants.labels["editError"]);
  11065. }
  11066. }
  11067. else {
  11068. alert(this.constants.labels["editBoundError"]);
  11069. }
  11070. },
  11071. /**
  11072. * delete everything in the selection
  11073. *
  11074. * @private
  11075. */
  11076. _deleteSelected : function() {
  11077. if (!this._selectionIsEmpty() && this.editMode == true) {
  11078. if (!this._clusterInSelection()) {
  11079. var selectedNodes = this.getSelectedNodes();
  11080. var selectedEdges = this.getSelectedEdges();
  11081. if (this.triggerFunctions.del) {
  11082. var me = this;
  11083. var data = {nodes: selectedNodes, edges: selectedEdges};
  11084. if (this.triggerFunctions.del.length = 2) {
  11085. this.triggerFunctions.del(data, function (finalizedData) {
  11086. me.edgesData.remove(finalizedData.edges);
  11087. me.nodesData.remove(finalizedData.nodes);
  11088. me._unselectAll();
  11089. me.moving = true;
  11090. me.start();
  11091. });
  11092. }
  11093. else {
  11094. alert(this.constants.labels["deleteError"])
  11095. }
  11096. }
  11097. else {
  11098. this.edgesData.remove(selectedEdges);
  11099. this.nodesData.remove(selectedNodes);
  11100. this._unselectAll();
  11101. this.moving = true;
  11102. this.start();
  11103. }
  11104. }
  11105. else {
  11106. alert(this.constants.labels["deleteClusterError"]);
  11107. }
  11108. }
  11109. }
  11110. };
  11111. /**
  11112. * Creation of the SectorMixin var.
  11113. *
  11114. * This contains all the functions the Graph object can use to employ the sector system.
  11115. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  11116. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  11117. *
  11118. * Alex de Mulder
  11119. * 21-01-2013
  11120. */
  11121. var SectorMixin = {
  11122. /**
  11123. * This function is only called by the setData function of the Graph object.
  11124. * This loads the global references into the active sector. This initializes the sector.
  11125. *
  11126. * @private
  11127. */
  11128. _putDataInSector : function() {
  11129. this.sectors["active"][this._sector()].nodes = this.nodes;
  11130. this.sectors["active"][this._sector()].edges = this.edges;
  11131. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  11132. },
  11133. /**
  11134. * /**
  11135. * This function sets the global references to nodes, edges and nodeIndices back to
  11136. * those of the supplied (active) sector. If a type is defined, do the specific type
  11137. *
  11138. * @param {String} sectorId
  11139. * @param {String} [sectorType] | "active" or "frozen"
  11140. * @private
  11141. */
  11142. _switchToSector : function(sectorId, sectorType) {
  11143. if (sectorType === undefined || sectorType == "active") {
  11144. this._switchToActiveSector(sectorId);
  11145. }
  11146. else {
  11147. this._switchToFrozenSector(sectorId);
  11148. }
  11149. },
  11150. /**
  11151. * This function sets the global references to nodes, edges and nodeIndices back to
  11152. * those of the supplied active sector.
  11153. *
  11154. * @param sectorId
  11155. * @private
  11156. */
  11157. _switchToActiveSector : function(sectorId) {
  11158. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  11159. this.nodes = this.sectors["active"][sectorId]["nodes"];
  11160. this.edges = this.sectors["active"][sectorId]["edges"];
  11161. },
  11162. /**
  11163. * This function sets the global references to nodes, edges and nodeIndices back to
  11164. * those of the supplied active sector.
  11165. *
  11166. * @param sectorId
  11167. * @private
  11168. */
  11169. _switchToSupportSector : function() {
  11170. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  11171. this.nodes = this.sectors["support"]["nodes"];
  11172. this.edges = this.sectors["support"]["edges"];
  11173. },
  11174. /**
  11175. * This function sets the global references to nodes, edges and nodeIndices back to
  11176. * those of the supplied frozen sector.
  11177. *
  11178. * @param sectorId
  11179. * @private
  11180. */
  11181. _switchToFrozenSector : function(sectorId) {
  11182. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  11183. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  11184. this.edges = this.sectors["frozen"][sectorId]["edges"];
  11185. },
  11186. /**
  11187. * This function sets the global references to nodes, edges and nodeIndices back to
  11188. * those of the currently active sector.
  11189. *
  11190. * @private
  11191. */
  11192. _loadLatestSector : function() {
  11193. this._switchToSector(this._sector());
  11194. },
  11195. /**
  11196. * This function returns the currently active sector Id
  11197. *
  11198. * @returns {String}
  11199. * @private
  11200. */
  11201. _sector : function() {
  11202. return this.activeSector[this.activeSector.length-1];
  11203. },
  11204. /**
  11205. * This function returns the previously active sector Id
  11206. *
  11207. * @returns {String}
  11208. * @private
  11209. */
  11210. _previousSector : function() {
  11211. if (this.activeSector.length > 1) {
  11212. return this.activeSector[this.activeSector.length-2];
  11213. }
  11214. else {
  11215. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  11216. }
  11217. },
  11218. /**
  11219. * We add the active sector at the end of the this.activeSector array
  11220. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  11221. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  11222. *
  11223. * @param newId
  11224. * @private
  11225. */
  11226. _setActiveSector : function(newId) {
  11227. this.activeSector.push(newId);
  11228. },
  11229. /**
  11230. * We remove the currently active sector id from the active sector stack. This happens when
  11231. * we reactivate the previously active sector
  11232. *
  11233. * @private
  11234. */
  11235. _forgetLastSector : function() {
  11236. this.activeSector.pop();
  11237. },
  11238. /**
  11239. * This function creates a new active sector with the supplied newId. This newId
  11240. * is the expanding node id.
  11241. *
  11242. * @param {String} newId | Id of the new active sector
  11243. * @private
  11244. */
  11245. _createNewSector : function(newId) {
  11246. // create the new sector
  11247. this.sectors["active"][newId] = {"nodes":{},
  11248. "edges":{},
  11249. "nodeIndices":[],
  11250. "formationScale": this.scale,
  11251. "drawingNode": undefined};
  11252. // create the new sector render node. This gives visual feedback that you are in a new sector.
  11253. this.sectors["active"][newId]['drawingNode'] = new Node(
  11254. {id:newId,
  11255. color: {
  11256. background: "#eaefef",
  11257. border: "495c5e"
  11258. }
  11259. },{},{},this.constants);
  11260. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  11261. },
  11262. /**
  11263. * This function removes the currently active sector. This is called when we create a new
  11264. * active sector.
  11265. *
  11266. * @param {String} sectorId | Id of the active sector that will be removed
  11267. * @private
  11268. */
  11269. _deleteActiveSector : function(sectorId) {
  11270. delete this.sectors["active"][sectorId];
  11271. },
  11272. /**
  11273. * This function removes the currently active sector. This is called when we reactivate
  11274. * the previously active sector.
  11275. *
  11276. * @param {String} sectorId | Id of the active sector that will be removed
  11277. * @private
  11278. */
  11279. _deleteFrozenSector : function(sectorId) {
  11280. delete this.sectors["frozen"][sectorId];
  11281. },
  11282. /**
  11283. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  11284. * We copy the references, then delete the active entree.
  11285. *
  11286. * @param sectorId
  11287. * @private
  11288. */
  11289. _freezeSector : function(sectorId) {
  11290. // we move the set references from the active to the frozen stack.
  11291. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  11292. // we have moved the sector data into the frozen set, we now remove it from the active set
  11293. this._deleteActiveSector(sectorId);
  11294. },
  11295. /**
  11296. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  11297. * object to the "active" object.
  11298. *
  11299. * @param sectorId
  11300. * @private
  11301. */
  11302. _activateSector : function(sectorId) {
  11303. // we move the set references from the frozen to the active stack.
  11304. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  11305. // we have moved the sector data into the active set, we now remove it from the frozen stack
  11306. this._deleteFrozenSector(sectorId);
  11307. },
  11308. /**
  11309. * This function merges the data from the currently active sector with a frozen sector. This is used
  11310. * in the process of reverting back to the previously active sector.
  11311. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  11312. * upon the creation of a new active sector.
  11313. *
  11314. * @param sectorId
  11315. * @private
  11316. */
  11317. _mergeThisWithFrozen : function(sectorId) {
  11318. // copy all nodes
  11319. for (var nodeId in this.nodes) {
  11320. if (this.nodes.hasOwnProperty(nodeId)) {
  11321. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  11322. }
  11323. }
  11324. // copy all edges (if not fully clustered, else there are no edges)
  11325. for (var edgeId in this.edges) {
  11326. if (this.edges.hasOwnProperty(edgeId)) {
  11327. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  11328. }
  11329. }
  11330. // merge the nodeIndices
  11331. for (var i = 0; i < this.nodeIndices.length; i++) {
  11332. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  11333. }
  11334. },
  11335. /**
  11336. * This clusters the sector to one cluster. It was a single cluster before this process started so
  11337. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  11338. *
  11339. * @private
  11340. */
  11341. _collapseThisToSingleCluster : function() {
  11342. this.clusterToFit(1,false);
  11343. },
  11344. /**
  11345. * We create a new active sector from the node that we want to open.
  11346. *
  11347. * @param node
  11348. * @private
  11349. */
  11350. _addSector : function(node) {
  11351. // this is the currently active sector
  11352. var sector = this._sector();
  11353. // // this should allow me to select nodes from a frozen set.
  11354. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  11355. // console.log("the node is part of the active sector");
  11356. // }
  11357. // else {
  11358. // console.log("I dont know what the fuck happened!!");
  11359. // }
  11360. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  11361. delete this.nodes[node.id];
  11362. var unqiueIdentifier = util.randomUUID();
  11363. // we fully freeze the currently active sector
  11364. this._freezeSector(sector);
  11365. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  11366. this._createNewSector(unqiueIdentifier);
  11367. // we add the active sector to the sectors array to be able to revert these steps later on
  11368. this._setActiveSector(unqiueIdentifier);
  11369. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  11370. this._switchToSector(this._sector());
  11371. // finally we add the node we removed from our previous active sector to the new active sector
  11372. this.nodes[node.id] = node;
  11373. },
  11374. /**
  11375. * We close the sector that is currently open and revert back to the one before.
  11376. * If the active sector is the "default" sector, nothing happens.
  11377. *
  11378. * @private
  11379. */
  11380. _collapseSector : function() {
  11381. // the currently active sector
  11382. var sector = this._sector();
  11383. // we cannot collapse the default sector
  11384. if (sector != "default") {
  11385. if ((this.nodeIndices.length == 1) ||
  11386. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11387. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11388. var previousSector = this._previousSector();
  11389. // we collapse the sector back to a single cluster
  11390. this._collapseThisToSingleCluster();
  11391. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  11392. // This previous sector is the one we will reactivate
  11393. this._mergeThisWithFrozen(previousSector);
  11394. // the previously active (frozen) sector now has all the data from the currently active sector.
  11395. // we can now delete the active sector.
  11396. this._deleteActiveSector(sector);
  11397. // we activate the previously active (and currently frozen) sector.
  11398. this._activateSector(previousSector);
  11399. // we load the references from the newly active sector into the global references
  11400. this._switchToSector(previousSector);
  11401. // we forget the previously active sector because we reverted to the one before
  11402. this._forgetLastSector();
  11403. // finally, we update the node index list.
  11404. this._updateNodeIndexList();
  11405. // we refresh the list with calulation nodes and calculation node indices.
  11406. this._updateCalculationNodes();
  11407. }
  11408. }
  11409. },
  11410. /**
  11411. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11412. *
  11413. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11414. * | we dont pass the function itself because then the "this" is the window object
  11415. * | instead of the Graph object
  11416. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11417. * @private
  11418. */
  11419. _doInAllActiveSectors : function(runFunction,argument) {
  11420. if (argument === undefined) {
  11421. for (var sector in this.sectors["active"]) {
  11422. if (this.sectors["active"].hasOwnProperty(sector)) {
  11423. // switch the global references to those of this sector
  11424. this._switchToActiveSector(sector);
  11425. this[runFunction]();
  11426. }
  11427. }
  11428. }
  11429. else {
  11430. for (var sector in this.sectors["active"]) {
  11431. if (this.sectors["active"].hasOwnProperty(sector)) {
  11432. // switch the global references to those of this sector
  11433. this._switchToActiveSector(sector);
  11434. var args = Array.prototype.splice.call(arguments, 1);
  11435. if (args.length > 1) {
  11436. this[runFunction](args[0],args[1]);
  11437. }
  11438. else {
  11439. this[runFunction](argument);
  11440. }
  11441. }
  11442. }
  11443. }
  11444. // we revert the global references back to our active sector
  11445. this._loadLatestSector();
  11446. },
  11447. /**
  11448. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11449. *
  11450. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11451. * | we dont pass the function itself because then the "this" is the window object
  11452. * | instead of the Graph object
  11453. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11454. * @private
  11455. */
  11456. _doInSupportSector : function(runFunction,argument) {
  11457. if (argument === undefined) {
  11458. this._switchToSupportSector();
  11459. this[runFunction]();
  11460. }
  11461. else {
  11462. this._switchToSupportSector();
  11463. var args = Array.prototype.splice.call(arguments, 1);
  11464. if (args.length > 1) {
  11465. this[runFunction](args[0],args[1]);
  11466. }
  11467. else {
  11468. this[runFunction](argument);
  11469. }
  11470. }
  11471. // we revert the global references back to our active sector
  11472. this._loadLatestSector();
  11473. },
  11474. /**
  11475. * This runs a function in all frozen sectors. This is used in the _redraw().
  11476. *
  11477. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11478. * | we don't pass the function itself because then the "this" is the window object
  11479. * | instead of the Graph object
  11480. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11481. * @private
  11482. */
  11483. _doInAllFrozenSectors : function(runFunction,argument) {
  11484. if (argument === undefined) {
  11485. for (var sector in this.sectors["frozen"]) {
  11486. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11487. // switch the global references to those of this sector
  11488. this._switchToFrozenSector(sector);
  11489. this[runFunction]();
  11490. }
  11491. }
  11492. }
  11493. else {
  11494. for (var sector in this.sectors["frozen"]) {
  11495. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11496. // switch the global references to those of this sector
  11497. this._switchToFrozenSector(sector);
  11498. var args = Array.prototype.splice.call(arguments, 1);
  11499. if (args.length > 1) {
  11500. this[runFunction](args[0],args[1]);
  11501. }
  11502. else {
  11503. this[runFunction](argument);
  11504. }
  11505. }
  11506. }
  11507. }
  11508. this._loadLatestSector();
  11509. },
  11510. /**
  11511. * This runs a function in all sectors. This is used in the _redraw().
  11512. *
  11513. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11514. * | we don't pass the function itself because then the "this" is the window object
  11515. * | instead of the Graph object
  11516. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11517. * @private
  11518. */
  11519. _doInAllSectors : function(runFunction,argument) {
  11520. var args = Array.prototype.splice.call(arguments, 1);
  11521. if (argument === undefined) {
  11522. this._doInAllActiveSectors(runFunction);
  11523. this._doInAllFrozenSectors(runFunction);
  11524. }
  11525. else {
  11526. if (args.length > 1) {
  11527. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  11528. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  11529. }
  11530. else {
  11531. this._doInAllActiveSectors(runFunction,argument);
  11532. this._doInAllFrozenSectors(runFunction,argument);
  11533. }
  11534. }
  11535. },
  11536. /**
  11537. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  11538. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  11539. *
  11540. * @private
  11541. */
  11542. _clearNodeIndexList : function() {
  11543. var sector = this._sector();
  11544. this.sectors["active"][sector]["nodeIndices"] = [];
  11545. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  11546. },
  11547. /**
  11548. * Draw the encompassing sector node
  11549. *
  11550. * @param ctx
  11551. * @param sectorType
  11552. * @private
  11553. */
  11554. _drawSectorNodes : function(ctx,sectorType) {
  11555. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  11556. for (var sector in this.sectors[sectorType]) {
  11557. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  11558. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  11559. this._switchToSector(sector,sectorType);
  11560. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  11561. for (var nodeId in this.nodes) {
  11562. if (this.nodes.hasOwnProperty(nodeId)) {
  11563. node = this.nodes[nodeId];
  11564. node.resize(ctx);
  11565. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  11566. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  11567. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  11568. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  11569. }
  11570. }
  11571. node = this.sectors[sectorType][sector]["drawingNode"];
  11572. node.x = 0.5 * (maxX + minX);
  11573. node.y = 0.5 * (maxY + minY);
  11574. node.width = 2 * (node.x - minX);
  11575. node.height = 2 * (node.y - minY);
  11576. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  11577. node.setScale(this.scale);
  11578. node._drawCircle(ctx);
  11579. }
  11580. }
  11581. }
  11582. },
  11583. _drawAllSectorNodes : function(ctx) {
  11584. this._drawSectorNodes(ctx,"frozen");
  11585. this._drawSectorNodes(ctx,"active");
  11586. this._loadLatestSector();
  11587. }
  11588. };
  11589. /**
  11590. * Creation of the ClusterMixin var.
  11591. *
  11592. * This contains all the functions the Graph object can use to employ clustering
  11593. *
  11594. * Alex de Mulder
  11595. * 21-01-2013
  11596. */
  11597. var ClusterMixin = {
  11598. /**
  11599. * This is only called in the constructor of the graph object
  11600. *
  11601. */
  11602. startWithClustering : function() {
  11603. // cluster if the data set is big
  11604. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  11605. // updates the lables after clustering
  11606. this.updateLabels();
  11607. // this is called here because if clusterin is disabled, the start and stabilize are called in
  11608. // the setData function.
  11609. if (this.stabilize) {
  11610. this._stabilize();
  11611. }
  11612. this.start();
  11613. },
  11614. /**
  11615. * This function clusters until the initialMaxNodes has been reached
  11616. *
  11617. * @param {Number} maxNumberOfNodes
  11618. * @param {Boolean} reposition
  11619. */
  11620. clusterToFit : function(maxNumberOfNodes, reposition) {
  11621. var numberOfNodes = this.nodeIndices.length;
  11622. var maxLevels = 50;
  11623. var level = 0;
  11624. // we first cluster the hubs, then we pull in the outliers, repeat
  11625. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  11626. if (level % 3 == 0) {
  11627. this.forceAggregateHubs(true);
  11628. this.normalizeClusterLevels();
  11629. }
  11630. else {
  11631. this.increaseClusterLevel(); // this also includes a cluster normalization
  11632. }
  11633. numberOfNodes = this.nodeIndices.length;
  11634. level += 1;
  11635. }
  11636. // after the clustering we reposition the nodes to reduce the initial chaos
  11637. if (level > 0 && reposition == true) {
  11638. this.repositionNodes();
  11639. }
  11640. this._updateCalculationNodes();
  11641. },
  11642. /**
  11643. * This function can be called to open up a specific cluster. It is only called by
  11644. * It will unpack the cluster back one level.
  11645. *
  11646. * @param node | Node object: cluster to open.
  11647. */
  11648. openCluster : function(node) {
  11649. var isMovingBeforeClustering = this.moving;
  11650. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  11651. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  11652. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  11653. this._addSector(node);
  11654. var level = 0;
  11655. // we decluster until we reach a decent number of nodes
  11656. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  11657. this.decreaseClusterLevel();
  11658. level += 1;
  11659. }
  11660. }
  11661. else {
  11662. this._expandClusterNode(node,false,true);
  11663. // update the index list, dynamic edges and labels
  11664. this._updateNodeIndexList();
  11665. this._updateDynamicEdges();
  11666. this._updateCalculationNodes();
  11667. this.updateLabels();
  11668. }
  11669. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11670. if (this.moving != isMovingBeforeClustering) {
  11671. this.start();
  11672. }
  11673. },
  11674. /**
  11675. * This calls the updateClustes with default arguments
  11676. */
  11677. updateClustersDefault : function() {
  11678. if (this.constants.clustering.enabled == true) {
  11679. this.updateClusters(0,false,false);
  11680. }
  11681. },
  11682. /**
  11683. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  11684. * be clustered with their connected node. This can be repeated as many times as needed.
  11685. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  11686. */
  11687. increaseClusterLevel : function() {
  11688. this.updateClusters(-1,false,true);
  11689. },
  11690. /**
  11691. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  11692. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  11693. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  11694. */
  11695. decreaseClusterLevel : function() {
  11696. this.updateClusters(1,false,true);
  11697. },
  11698. /**
  11699. * This is the main clustering function. It clusters and declusters on zoom or forced
  11700. * This function clusters on zoom, it can be called with a predefined zoom direction
  11701. * If out, check if we can form clusters, if in, check if we can open clusters.
  11702. * This function is only called from _zoom()
  11703. *
  11704. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  11705. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  11706. * @param {Boolean} force | enabled or disable forcing
  11707. *
  11708. */
  11709. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  11710. var isMovingBeforeClustering = this.moving;
  11711. var amountOfNodes = this.nodeIndices.length;
  11712. // on zoom out collapse the sector if the scale is at the level the sector was made
  11713. if (this.previousScale > this.scale && zoomDirection == 0) {
  11714. this._collapseSector();
  11715. }
  11716. // check if we zoom in or out
  11717. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  11718. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  11719. // outer nodes determines if it is being clustered
  11720. this._formClusters(force);
  11721. }
  11722. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  11723. if (force == true) {
  11724. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  11725. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  11726. this._openClusters(recursive,force);
  11727. }
  11728. else {
  11729. // if a cluster takes up a set percentage of the active window
  11730. this._openClustersBySize();
  11731. }
  11732. }
  11733. this._updateNodeIndexList();
  11734. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  11735. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  11736. this._aggregateHubs(force);
  11737. this._updateNodeIndexList();
  11738. }
  11739. // we now reduce chains.
  11740. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  11741. this.handleChains();
  11742. this._updateNodeIndexList();
  11743. }
  11744. this.previousScale = this.scale;
  11745. // rest of the update the index list, dynamic edges and labels
  11746. this._updateDynamicEdges();
  11747. this.updateLabels();
  11748. // if a cluster was formed, we increase the clusterSession
  11749. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  11750. this.clusterSession += 1;
  11751. // if clusters have been made, we normalize the cluster level
  11752. this.normalizeClusterLevels();
  11753. }
  11754. if (doNotStart == false || doNotStart === undefined) {
  11755. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11756. if (this.moving != isMovingBeforeClustering) {
  11757. this.start();
  11758. }
  11759. }
  11760. this._updateCalculationNodes();
  11761. },
  11762. /**
  11763. * This function handles the chains. It is called on every updateClusters().
  11764. */
  11765. handleChains : function() {
  11766. // after clustering we check how many chains there are
  11767. var chainPercentage = this._getChainFraction();
  11768. if (chainPercentage > this.constants.clustering.chainThreshold) {
  11769. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  11770. }
  11771. },
  11772. /**
  11773. * this functions starts clustering by hubs
  11774. * The minimum hub threshold is set globally
  11775. *
  11776. * @private
  11777. */
  11778. _aggregateHubs : function(force) {
  11779. this._getHubSize();
  11780. this._formClustersByHub(force,false);
  11781. },
  11782. /**
  11783. * This function is fired by keypress. It forces hubs to form.
  11784. *
  11785. */
  11786. forceAggregateHubs : function(doNotStart) {
  11787. var isMovingBeforeClustering = this.moving;
  11788. var amountOfNodes = this.nodeIndices.length;
  11789. this._aggregateHubs(true);
  11790. // update the index list, dynamic edges and labels
  11791. this._updateNodeIndexList();
  11792. this._updateDynamicEdges();
  11793. this.updateLabels();
  11794. // if a cluster was formed, we increase the clusterSession
  11795. if (this.nodeIndices.length != amountOfNodes) {
  11796. this.clusterSession += 1;
  11797. }
  11798. if (doNotStart == false || doNotStart === undefined) {
  11799. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11800. if (this.moving != isMovingBeforeClustering) {
  11801. this.start();
  11802. }
  11803. }
  11804. },
  11805. /**
  11806. * If a cluster takes up more than a set percentage of the screen, open the cluster
  11807. *
  11808. * @private
  11809. */
  11810. _openClustersBySize : function() {
  11811. for (var nodeId in this.nodes) {
  11812. if (this.nodes.hasOwnProperty(nodeId)) {
  11813. var node = this.nodes[nodeId];
  11814. if (node.inView() == true) {
  11815. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11816. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11817. this.openCluster(node);
  11818. }
  11819. }
  11820. }
  11821. }
  11822. },
  11823. /**
  11824. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  11825. * has to be opened based on the current zoom level.
  11826. *
  11827. * @private
  11828. */
  11829. _openClusters : function(recursive,force) {
  11830. for (var i = 0; i < this.nodeIndices.length; i++) {
  11831. var node = this.nodes[this.nodeIndices[i]];
  11832. this._expandClusterNode(node,recursive,force);
  11833. this._updateCalculationNodes();
  11834. }
  11835. },
  11836. /**
  11837. * This function checks if a node has to be opened. This is done by checking the zoom level.
  11838. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  11839. * This recursive behaviour is optional and can be set by the recursive argument.
  11840. *
  11841. * @param {Node} parentNode | to check for cluster and expand
  11842. * @param {Boolean} recursive | enabled or disable recursive calling
  11843. * @param {Boolean} force | enabled or disable forcing
  11844. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  11845. * @private
  11846. */
  11847. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  11848. // first check if node is a cluster
  11849. if (parentNode.clusterSize > 1) {
  11850. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  11851. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  11852. openAll = true;
  11853. }
  11854. recursive = openAll ? true : recursive;
  11855. // if the last child has been added on a smaller scale than current scale decluster
  11856. if (parentNode.formationScale < this.scale || force == true) {
  11857. // we will check if any of the contained child nodes should be removed from the cluster
  11858. for (var containedNodeId in parentNode.containedNodes) {
  11859. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  11860. var childNode = parentNode.containedNodes[containedNodeId];
  11861. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  11862. // the largest cluster is the one that comes from outside
  11863. if (force == true) {
  11864. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  11865. || openAll) {
  11866. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  11867. }
  11868. }
  11869. else {
  11870. if (this._nodeInActiveArea(parentNode)) {
  11871. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  11872. }
  11873. }
  11874. }
  11875. }
  11876. }
  11877. }
  11878. },
  11879. /**
  11880. * ONLY CALLED FROM _expandClusterNode
  11881. *
  11882. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  11883. * the child node from the parent contained_node object and put it back into the global nodes object.
  11884. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  11885. *
  11886. * @param {Node} parentNode | the parent node
  11887. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  11888. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  11889. * With force and recursive both true, the entire cluster is unpacked
  11890. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  11891. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  11892. * @private
  11893. */
  11894. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  11895. var childNode = parentNode.containedNodes[containedNodeId];
  11896. // if child node has been added on smaller scale than current, kick out
  11897. if (childNode.formationScale < this.scale || force == true) {
  11898. // unselect all selected items
  11899. this._unselectAll();
  11900. // put the child node back in the global nodes object
  11901. this.nodes[containedNodeId] = childNode;
  11902. // release the contained edges from this childNode back into the global edges
  11903. this._releaseContainedEdges(parentNode,childNode);
  11904. // reconnect rerouted edges to the childNode
  11905. this._connectEdgeBackToChild(parentNode,childNode);
  11906. // validate all edges in dynamicEdges
  11907. this._validateEdges(parentNode);
  11908. // undo the changes from the clustering operation on the parent node
  11909. parentNode.mass -= childNode.mass;
  11910. parentNode.clusterSize -= childNode.clusterSize;
  11911. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  11912. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  11913. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  11914. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  11915. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  11916. // remove node from the list
  11917. delete parentNode.containedNodes[containedNodeId];
  11918. // check if there are other childs with this clusterSession in the parent.
  11919. var othersPresent = false;
  11920. for (var childNodeId in parentNode.containedNodes) {
  11921. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  11922. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  11923. othersPresent = true;
  11924. break;
  11925. }
  11926. }
  11927. }
  11928. // if there are no others, remove the cluster session from the list
  11929. if (othersPresent == false) {
  11930. parentNode.clusterSessions.pop();
  11931. }
  11932. this._repositionBezierNodes(childNode);
  11933. // this._repositionBezierNodes(parentNode);
  11934. // remove the clusterSession from the child node
  11935. childNode.clusterSession = 0;
  11936. // recalculate the size of the node on the next time the node is rendered
  11937. parentNode.clearSizeCache();
  11938. // restart the simulation to reorganise all nodes
  11939. this.moving = true;
  11940. }
  11941. // check if a further expansion step is possible if recursivity is enabled
  11942. if (recursive == true) {
  11943. this._expandClusterNode(childNode,recursive,force,openAll);
  11944. }
  11945. },
  11946. /**
  11947. * position the bezier nodes at the center of the edges
  11948. *
  11949. * @param node
  11950. * @private
  11951. */
  11952. _repositionBezierNodes : function(node) {
  11953. for (var i = 0; i < node.dynamicEdges.length; i++) {
  11954. node.dynamicEdges[i].positionBezierNode();
  11955. }
  11956. },
  11957. /**
  11958. * This function checks if any nodes at the end of their trees have edges below a threshold length
  11959. * This function is called only from updateClusters()
  11960. * forceLevelCollapse ignores the length of the edge and collapses one level
  11961. * This means that a node with only one edge will be clustered with its connected node
  11962. *
  11963. * @private
  11964. * @param {Boolean} force
  11965. */
  11966. _formClusters : function(force) {
  11967. if (force == false) {
  11968. this._formClustersByZoom();
  11969. }
  11970. else {
  11971. this._forceClustersByZoom();
  11972. }
  11973. },
  11974. /**
  11975. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  11976. *
  11977. * @private
  11978. */
  11979. _formClustersByZoom : function() {
  11980. var dx,dy,length,
  11981. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  11982. // check if any edges are shorter than minLength and start the clustering
  11983. // the clustering favours the node with the larger mass
  11984. for (var edgeId in this.edges) {
  11985. if (this.edges.hasOwnProperty(edgeId)) {
  11986. var edge = this.edges[edgeId];
  11987. if (edge.connected) {
  11988. if (edge.toId != edge.fromId) {
  11989. dx = (edge.to.x - edge.from.x);
  11990. dy = (edge.to.y - edge.from.y);
  11991. length = Math.sqrt(dx * dx + dy * dy);
  11992. if (length < minLength) {
  11993. // first check which node is larger
  11994. var parentNode = edge.from;
  11995. var childNode = edge.to;
  11996. if (edge.to.mass > edge.from.mass) {
  11997. parentNode = edge.to;
  11998. childNode = edge.from;
  11999. }
  12000. if (childNode.dynamicEdgesLength == 1) {
  12001. this._addToCluster(parentNode,childNode,false);
  12002. }
  12003. else if (parentNode.dynamicEdgesLength == 1) {
  12004. this._addToCluster(childNode,parentNode,false);
  12005. }
  12006. }
  12007. }
  12008. }
  12009. }
  12010. }
  12011. },
  12012. /**
  12013. * This function forces the graph to cluster all nodes with only one connecting edge to their
  12014. * connected node.
  12015. *
  12016. * @private
  12017. */
  12018. _forceClustersByZoom : function() {
  12019. for (var nodeId in this.nodes) {
  12020. // another node could have absorbed this child.
  12021. if (this.nodes.hasOwnProperty(nodeId)) {
  12022. var childNode = this.nodes[nodeId];
  12023. // the edges can be swallowed by another decrease
  12024. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  12025. var edge = childNode.dynamicEdges[0];
  12026. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  12027. // group to the largest node
  12028. if (childNode.id != parentNode.id) {
  12029. if (parentNode.mass > childNode.mass) {
  12030. this._addToCluster(parentNode,childNode,true);
  12031. }
  12032. else {
  12033. this._addToCluster(childNode,parentNode,true);
  12034. }
  12035. }
  12036. }
  12037. }
  12038. }
  12039. },
  12040. /**
  12041. * To keep the nodes of roughly equal size we normalize the cluster levels.
  12042. * This function clusters a node to its smallest connected neighbour.
  12043. *
  12044. * @param node
  12045. * @private
  12046. */
  12047. _clusterToSmallestNeighbour : function(node) {
  12048. var smallestNeighbour = -1;
  12049. var smallestNeighbourNode = null;
  12050. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12051. if (node.dynamicEdges[i] !== undefined) {
  12052. var neighbour = null;
  12053. if (node.dynamicEdges[i].fromId != node.id) {
  12054. neighbour = node.dynamicEdges[i].from;
  12055. }
  12056. else if (node.dynamicEdges[i].toId != node.id) {
  12057. neighbour = node.dynamicEdges[i].to;
  12058. }
  12059. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  12060. smallestNeighbour = neighbour.clusterSessions.length;
  12061. smallestNeighbourNode = neighbour;
  12062. }
  12063. }
  12064. }
  12065. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  12066. this._addToCluster(neighbour, node, true);
  12067. }
  12068. },
  12069. /**
  12070. * This function forms clusters from hubs, it loops over all nodes
  12071. *
  12072. * @param {Boolean} force | Disregard zoom level
  12073. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12074. * @private
  12075. */
  12076. _formClustersByHub : function(force, onlyEqual) {
  12077. // we loop over all nodes in the list
  12078. for (var nodeId in this.nodes) {
  12079. // we check if it is still available since it can be used by the clustering in this loop
  12080. if (this.nodes.hasOwnProperty(nodeId)) {
  12081. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  12082. }
  12083. }
  12084. },
  12085. /**
  12086. * This function forms a cluster from a specific preselected hub node
  12087. *
  12088. * @param {Node} hubNode | the node we will cluster as a hub
  12089. * @param {Boolean} force | Disregard zoom level
  12090. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12091. * @param {Number} [absorptionSizeOffset] |
  12092. * @private
  12093. */
  12094. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  12095. if (absorptionSizeOffset === undefined) {
  12096. absorptionSizeOffset = 0;
  12097. }
  12098. // we decide if the node is a hub
  12099. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  12100. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  12101. // initialize variables
  12102. var dx,dy,length;
  12103. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12104. var allowCluster = false;
  12105. // we create a list of edges because the dynamicEdges change over the course of this loop
  12106. var edgesIdarray = [];
  12107. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  12108. for (var j = 0; j < amountOfInitialEdges; j++) {
  12109. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  12110. }
  12111. // if the hub clustering is not forces, we check if one of the edges connected
  12112. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  12113. if (force == false) {
  12114. allowCluster = false;
  12115. for (j = 0; j < amountOfInitialEdges; j++) {
  12116. var edge = this.edges[edgesIdarray[j]];
  12117. if (edge !== undefined) {
  12118. if (edge.connected) {
  12119. if (edge.toId != edge.fromId) {
  12120. dx = (edge.to.x - edge.from.x);
  12121. dy = (edge.to.y - edge.from.y);
  12122. length = Math.sqrt(dx * dx + dy * dy);
  12123. if (length < minLength) {
  12124. allowCluster = true;
  12125. break;
  12126. }
  12127. }
  12128. }
  12129. }
  12130. }
  12131. }
  12132. // start the clustering if allowed
  12133. if ((!force && allowCluster) || force) {
  12134. // we loop over all edges INITIALLY connected to this hub
  12135. for (j = 0; j < amountOfInitialEdges; j++) {
  12136. edge = this.edges[edgesIdarray[j]];
  12137. // the edge can be clustered by this function in a previous loop
  12138. if (edge !== undefined) {
  12139. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  12140. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  12141. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  12142. (childNode.id != hubNode.id)) {
  12143. this._addToCluster(hubNode,childNode,force);
  12144. }
  12145. }
  12146. }
  12147. }
  12148. }
  12149. },
  12150. /**
  12151. * This function adds the child node to the parent node, creating a cluster if it is not already.
  12152. *
  12153. * @param {Node} parentNode | this is the node that will house the child node
  12154. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  12155. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  12156. * @private
  12157. */
  12158. _addToCluster : function(parentNode, childNode, force) {
  12159. // join child node in the parent node
  12160. parentNode.containedNodes[childNode.id] = childNode;
  12161. // manage all the edges connected to the child and parent nodes
  12162. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  12163. var edge = childNode.dynamicEdges[i];
  12164. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  12165. this._addToContainedEdges(parentNode,childNode,edge);
  12166. }
  12167. else {
  12168. this._connectEdgeToCluster(parentNode,childNode,edge);
  12169. }
  12170. }
  12171. // a contained node has no dynamic edges.
  12172. childNode.dynamicEdges = [];
  12173. // remove circular edges from clusters
  12174. this._containCircularEdgesFromNode(parentNode,childNode);
  12175. // remove the childNode from the global nodes object
  12176. delete this.nodes[childNode.id];
  12177. // update the properties of the child and parent
  12178. var massBefore = parentNode.mass;
  12179. childNode.clusterSession = this.clusterSession;
  12180. parentNode.mass += childNode.mass;
  12181. parentNode.clusterSize += childNode.clusterSize;
  12182. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12183. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  12184. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  12185. parentNode.clusterSessions.push(this.clusterSession);
  12186. }
  12187. // forced clusters only open from screen size and double tap
  12188. if (force == true) {
  12189. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  12190. parentNode.formationScale = 0;
  12191. }
  12192. else {
  12193. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  12194. }
  12195. // recalculate the size of the node on the next time the node is rendered
  12196. parentNode.clearSizeCache();
  12197. // set the pop-out scale for the childnode
  12198. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  12199. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  12200. childNode.clearVelocity();
  12201. // the mass has altered, preservation of energy dictates the velocity to be updated
  12202. parentNode.updateVelocity(massBefore);
  12203. // restart the simulation to reorganise all nodes
  12204. this.moving = true;
  12205. },
  12206. /**
  12207. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  12208. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  12209. * It has to be called if a level is collapsed. It is called by _formClusters().
  12210. * @private
  12211. */
  12212. _updateDynamicEdges : function() {
  12213. for (var i = 0; i < this.nodeIndices.length; i++) {
  12214. var node = this.nodes[this.nodeIndices[i]];
  12215. node.dynamicEdgesLength = node.dynamicEdges.length;
  12216. // this corrects for multiple edges pointing at the same other node
  12217. var correction = 0;
  12218. if (node.dynamicEdgesLength > 1) {
  12219. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  12220. var edgeToId = node.dynamicEdges[j].toId;
  12221. var edgeFromId = node.dynamicEdges[j].fromId;
  12222. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  12223. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  12224. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  12225. correction += 1;
  12226. }
  12227. }
  12228. }
  12229. }
  12230. node.dynamicEdgesLength -= correction;
  12231. }
  12232. },
  12233. /**
  12234. * This adds an edge from the childNode to the contained edges of the parent node
  12235. *
  12236. * @param parentNode | Node object
  12237. * @param childNode | Node object
  12238. * @param edge | Edge object
  12239. * @private
  12240. */
  12241. _addToContainedEdges : function(parentNode, childNode, edge) {
  12242. // create an array object if it does not yet exist for this childNode
  12243. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  12244. parentNode.containedEdges[childNode.id] = []
  12245. }
  12246. // add this edge to the list
  12247. parentNode.containedEdges[childNode.id].push(edge);
  12248. // remove the edge from the global edges object
  12249. delete this.edges[edge.id];
  12250. // remove the edge from the parent object
  12251. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12252. if (parentNode.dynamicEdges[i].id == edge.id) {
  12253. parentNode.dynamicEdges.splice(i,1);
  12254. break;
  12255. }
  12256. }
  12257. },
  12258. /**
  12259. * This function connects an edge that was connected to a child node to the parent node.
  12260. * It keeps track of which nodes it has been connected to with the originalId array.
  12261. *
  12262. * @param {Node} parentNode | Node object
  12263. * @param {Node} childNode | Node object
  12264. * @param {Edge} edge | Edge object
  12265. * @private
  12266. */
  12267. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  12268. // handle circular edges
  12269. if (edge.toId == edge.fromId) {
  12270. this._addToContainedEdges(parentNode, childNode, edge);
  12271. }
  12272. else {
  12273. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  12274. edge.originalToId.push(childNode.id);
  12275. edge.to = parentNode;
  12276. edge.toId = parentNode.id;
  12277. }
  12278. else { // edge connected to other node with the "from" side
  12279. edge.originalFromId.push(childNode.id);
  12280. edge.from = parentNode;
  12281. edge.fromId = parentNode.id;
  12282. }
  12283. this._addToReroutedEdges(parentNode,childNode,edge);
  12284. }
  12285. },
  12286. /**
  12287. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  12288. * these edges inside of the cluster.
  12289. *
  12290. * @param parentNode
  12291. * @param childNode
  12292. * @private
  12293. */
  12294. _containCircularEdgesFromNode : function(parentNode, childNode) {
  12295. // manage all the edges connected to the child and parent nodes
  12296. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12297. var edge = parentNode.dynamicEdges[i];
  12298. // handle circular edges
  12299. if (edge.toId == edge.fromId) {
  12300. this._addToContainedEdges(parentNode, childNode, edge);
  12301. }
  12302. }
  12303. },
  12304. /**
  12305. * This adds an edge from the childNode to the rerouted edges of the parent node
  12306. *
  12307. * @param parentNode | Node object
  12308. * @param childNode | Node object
  12309. * @param edge | Edge object
  12310. * @private
  12311. */
  12312. _addToReroutedEdges : function(parentNode, childNode, edge) {
  12313. // create an array object if it does not yet exist for this childNode
  12314. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  12315. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  12316. parentNode.reroutedEdges[childNode.id] = [];
  12317. }
  12318. parentNode.reroutedEdges[childNode.id].push(edge);
  12319. // this edge becomes part of the dynamicEdges of the cluster node
  12320. parentNode.dynamicEdges.push(edge);
  12321. },
  12322. /**
  12323. * This function connects an edge that was connected to a cluster node back to the child node.
  12324. *
  12325. * @param parentNode | Node object
  12326. * @param childNode | Node object
  12327. * @private
  12328. */
  12329. _connectEdgeBackToChild : function(parentNode, childNode) {
  12330. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  12331. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  12332. var edge = parentNode.reroutedEdges[childNode.id][i];
  12333. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  12334. edge.originalFromId.pop();
  12335. edge.fromId = childNode.id;
  12336. edge.from = childNode;
  12337. }
  12338. else {
  12339. edge.originalToId.pop();
  12340. edge.toId = childNode.id;
  12341. edge.to = childNode;
  12342. }
  12343. // append this edge to the list of edges connecting to the childnode
  12344. childNode.dynamicEdges.push(edge);
  12345. // remove the edge from the parent object
  12346. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  12347. if (parentNode.dynamicEdges[j].id == edge.id) {
  12348. parentNode.dynamicEdges.splice(j,1);
  12349. break;
  12350. }
  12351. }
  12352. }
  12353. // remove the entry from the rerouted edges
  12354. delete parentNode.reroutedEdges[childNode.id];
  12355. }
  12356. },
  12357. /**
  12358. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  12359. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  12360. * parentNode
  12361. *
  12362. * @param parentNode | Node object
  12363. * @private
  12364. */
  12365. _validateEdges : function(parentNode) {
  12366. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12367. var edge = parentNode.dynamicEdges[i];
  12368. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  12369. parentNode.dynamicEdges.splice(i,1);
  12370. }
  12371. }
  12372. },
  12373. /**
  12374. * This function released the contained edges back into the global domain and puts them back into the
  12375. * dynamic edges of both parent and child.
  12376. *
  12377. * @param {Node} parentNode |
  12378. * @param {Node} childNode |
  12379. * @private
  12380. */
  12381. _releaseContainedEdges : function(parentNode, childNode) {
  12382. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  12383. var edge = parentNode.containedEdges[childNode.id][i];
  12384. // put the edge back in the global edges object
  12385. this.edges[edge.id] = edge;
  12386. // put the edge back in the dynamic edges of the child and parent
  12387. childNode.dynamicEdges.push(edge);
  12388. parentNode.dynamicEdges.push(edge);
  12389. }
  12390. // remove the entry from the contained edges
  12391. delete parentNode.containedEdges[childNode.id];
  12392. },
  12393. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  12394. /**
  12395. * This updates the node labels for all nodes (for debugging purposes)
  12396. */
  12397. updateLabels : function() {
  12398. var nodeId;
  12399. // update node labels
  12400. for (nodeId in this.nodes) {
  12401. if (this.nodes.hasOwnProperty(nodeId)) {
  12402. var node = this.nodes[nodeId];
  12403. if (node.clusterSize > 1) {
  12404. node.label = "[".concat(String(node.clusterSize),"]");
  12405. }
  12406. }
  12407. }
  12408. // update node labels
  12409. for (nodeId in this.nodes) {
  12410. if (this.nodes.hasOwnProperty(nodeId)) {
  12411. node = this.nodes[nodeId];
  12412. if (node.clusterSize == 1) {
  12413. if (node.originalLabel !== undefined) {
  12414. node.label = node.originalLabel;
  12415. }
  12416. else {
  12417. node.label = String(node.id);
  12418. }
  12419. }
  12420. }
  12421. }
  12422. // /* Debug Override */
  12423. // for (nodeId in this.nodes) {
  12424. // if (this.nodes.hasOwnProperty(nodeId)) {
  12425. // node = this.nodes[nodeId];
  12426. // node.label = String(node.level);
  12427. // }
  12428. // }
  12429. },
  12430. /**
  12431. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  12432. * if the rest of the nodes are already a few cluster levels in.
  12433. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  12434. * clustered enough to the clusterToSmallestNeighbours function.
  12435. */
  12436. normalizeClusterLevels : function() {
  12437. var maxLevel = 0;
  12438. var minLevel = 1e9;
  12439. var clusterLevel = 0;
  12440. // we loop over all nodes in the list
  12441. for (var nodeId in this.nodes) {
  12442. if (this.nodes.hasOwnProperty(nodeId)) {
  12443. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  12444. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  12445. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  12446. }
  12447. }
  12448. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  12449. var amountOfNodes = this.nodeIndices.length;
  12450. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  12451. // we loop over all nodes in the list
  12452. for (var nodeId in this.nodes) {
  12453. if (this.nodes.hasOwnProperty(nodeId)) {
  12454. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  12455. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  12456. }
  12457. }
  12458. }
  12459. this._updateNodeIndexList();
  12460. this._updateDynamicEdges();
  12461. // if a cluster was formed, we increase the clusterSession
  12462. if (this.nodeIndices.length != amountOfNodes) {
  12463. this.clusterSession += 1;
  12464. }
  12465. }
  12466. },
  12467. /**
  12468. * This function determines if the cluster we want to decluster is in the active area
  12469. * this means around the zoom center
  12470. *
  12471. * @param {Node} node
  12472. * @returns {boolean}
  12473. * @private
  12474. */
  12475. _nodeInActiveArea : function(node) {
  12476. return (
  12477. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12478. &&
  12479. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12480. )
  12481. },
  12482. /**
  12483. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  12484. * It puts large clusters away from the center and randomizes the order.
  12485. *
  12486. */
  12487. repositionNodes : function() {
  12488. for (var i = 0; i < this.nodeIndices.length; i++) {
  12489. var node = this.nodes[this.nodeIndices[i]];
  12490. if ((node.xFixed == false || node.yFixed == false)) {
  12491. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  12492. var angle = 2 * Math.PI * Math.random();
  12493. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  12494. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  12495. this._repositionBezierNodes(node);
  12496. }
  12497. }
  12498. },
  12499. /**
  12500. * We determine how many connections denote an important hub.
  12501. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  12502. *
  12503. * @private
  12504. */
  12505. _getHubSize : function() {
  12506. var average = 0;
  12507. var averageSquared = 0;
  12508. var hubCounter = 0;
  12509. var largestHub = 0;
  12510. for (var i = 0; i < this.nodeIndices.length; i++) {
  12511. var node = this.nodes[this.nodeIndices[i]];
  12512. if (node.dynamicEdgesLength > largestHub) {
  12513. largestHub = node.dynamicEdgesLength;
  12514. }
  12515. average += node.dynamicEdgesLength;
  12516. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  12517. hubCounter += 1;
  12518. }
  12519. average = average / hubCounter;
  12520. averageSquared = averageSquared / hubCounter;
  12521. var variance = averageSquared - Math.pow(average,2);
  12522. var standardDeviation = Math.sqrt(variance);
  12523. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  12524. // always have at least one to cluster
  12525. if (this.hubThreshold > largestHub) {
  12526. this.hubThreshold = largestHub;
  12527. }
  12528. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  12529. // console.log("hubThreshold:",this.hubThreshold);
  12530. },
  12531. /**
  12532. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12533. * with this amount we can cluster specifically on these chains.
  12534. *
  12535. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  12536. * @private
  12537. */
  12538. _reduceAmountOfChains : function(fraction) {
  12539. this.hubThreshold = 2;
  12540. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  12541. for (var nodeId in this.nodes) {
  12542. if (this.nodes.hasOwnProperty(nodeId)) {
  12543. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12544. if (reduceAmount > 0) {
  12545. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  12546. reduceAmount -= 1;
  12547. }
  12548. }
  12549. }
  12550. }
  12551. },
  12552. /**
  12553. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12554. * with this amount we can cluster specifically on these chains.
  12555. *
  12556. * @private
  12557. */
  12558. _getChainFraction : function() {
  12559. var chains = 0;
  12560. var total = 0;
  12561. for (var nodeId in this.nodes) {
  12562. if (this.nodes.hasOwnProperty(nodeId)) {
  12563. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12564. chains += 1;
  12565. }
  12566. total += 1;
  12567. }
  12568. }
  12569. return chains/total;
  12570. }
  12571. };
  12572. var SelectionMixin = {
  12573. /**
  12574. * This function can be called from the _doInAllSectors function
  12575. *
  12576. * @param object
  12577. * @param overlappingNodes
  12578. * @private
  12579. */
  12580. _getNodesOverlappingWith : function(object, overlappingNodes) {
  12581. var nodes = this.nodes;
  12582. for (var nodeId in nodes) {
  12583. if (nodes.hasOwnProperty(nodeId)) {
  12584. if (nodes[nodeId].isOverlappingWith(object)) {
  12585. overlappingNodes.push(nodeId);
  12586. }
  12587. }
  12588. }
  12589. },
  12590. /**
  12591. * retrieve all nodes overlapping with given object
  12592. * @param {Object} object An object with parameters left, top, right, bottom
  12593. * @return {Number[]} An array with id's of the overlapping nodes
  12594. * @private
  12595. */
  12596. _getAllNodesOverlappingWith : function (object) {
  12597. var overlappingNodes = [];
  12598. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  12599. return overlappingNodes;
  12600. },
  12601. /**
  12602. * Return a position object in canvasspace from a single point in screenspace
  12603. *
  12604. * @param pointer
  12605. * @returns {{left: number, top: number, right: number, bottom: number}}
  12606. * @private
  12607. */
  12608. _pointerToPositionObject : function(pointer) {
  12609. var x = this._canvasToX(pointer.x);
  12610. var y = this._canvasToY(pointer.y);
  12611. return {left: x,
  12612. top: y,
  12613. right: x,
  12614. bottom: y};
  12615. },
  12616. /**
  12617. * Get the top node at the a specific point (like a click)
  12618. *
  12619. * @param {{x: Number, y: Number}} pointer
  12620. * @return {Node | null} node
  12621. * @private
  12622. */
  12623. _getNodeAt : function (pointer) {
  12624. // we first check if this is an navigation controls element
  12625. var positionObject = this._pointerToPositionObject(pointer);
  12626. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  12627. // if there are overlapping nodes, select the last one, this is the
  12628. // one which is drawn on top of the others
  12629. if (overlappingNodes.length > 0) {
  12630. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  12631. }
  12632. else {
  12633. return null;
  12634. }
  12635. },
  12636. /**
  12637. * retrieve all edges overlapping with given object, selector is around center
  12638. * @param {Object} object An object with parameters left, top, right, bottom
  12639. * @return {Number[]} An array with id's of the overlapping nodes
  12640. * @private
  12641. */
  12642. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  12643. var edges = this.edges;
  12644. for (var edgeId in edges) {
  12645. if (edges.hasOwnProperty(edgeId)) {
  12646. if (edges[edgeId].isOverlappingWith(object)) {
  12647. overlappingEdges.push(edgeId);
  12648. }
  12649. }
  12650. }
  12651. },
  12652. /**
  12653. * retrieve all nodes overlapping with given object
  12654. * @param {Object} object An object with parameters left, top, right, bottom
  12655. * @return {Number[]} An array with id's of the overlapping nodes
  12656. * @private
  12657. */
  12658. _getAllEdgesOverlappingWith : function (object) {
  12659. var overlappingEdges = [];
  12660. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  12661. return overlappingEdges;
  12662. },
  12663. /**
  12664. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  12665. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  12666. *
  12667. * @param pointer
  12668. * @returns {null}
  12669. * @private
  12670. */
  12671. _getEdgeAt : function(pointer) {
  12672. var positionObject = this._pointerToPositionObject(pointer);
  12673. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  12674. if (overlappingEdges.length > 0) {
  12675. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  12676. }
  12677. else {
  12678. return null;
  12679. }
  12680. },
  12681. /**
  12682. * Add object to the selection array.
  12683. *
  12684. * @param obj
  12685. * @private
  12686. */
  12687. _addToSelection : function(obj) {
  12688. if (obj instanceof Node) {
  12689. this.selectionObj.nodes[obj.id] = obj;
  12690. }
  12691. else {
  12692. this.selectionObj.edges[obj.id] = obj;
  12693. }
  12694. },
  12695. /**
  12696. * Remove a single option from selection.
  12697. *
  12698. * @param {Object} obj
  12699. * @private
  12700. */
  12701. _removeFromSelection : function(obj) {
  12702. if (obj instanceof Node) {
  12703. delete this.selectionObj.nodes[obj.id];
  12704. }
  12705. else {
  12706. delete this.selectionObj.edges[obj.id];
  12707. }
  12708. },
  12709. /**
  12710. * Unselect all. The selectionObj is useful for this.
  12711. *
  12712. * @param {Boolean} [doNotTrigger] | ignore trigger
  12713. * @private
  12714. */
  12715. _unselectAll : function(doNotTrigger) {
  12716. if (doNotTrigger === undefined) {
  12717. doNotTrigger = false;
  12718. }
  12719. for(var nodeId in this.selectionObj.nodes) {
  12720. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12721. this.selectionObj.nodes[nodeId].unselect();
  12722. }
  12723. }
  12724. for(var edgeId in this.selectionObj.edges) {
  12725. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12726. this.selectionObj.edges[edgeId].unselect();;
  12727. }
  12728. }
  12729. this.selectionObj = {nodes:{},edges:{}};
  12730. if (doNotTrigger == false) {
  12731. this.emit('select', this.getSelection());
  12732. }
  12733. },
  12734. /**
  12735. * Unselect all clusters. The selectionObj is useful for this.
  12736. *
  12737. * @param {Boolean} [doNotTrigger] | ignore trigger
  12738. * @private
  12739. */
  12740. _unselectClusters : function(doNotTrigger) {
  12741. if (doNotTrigger === undefined) {
  12742. doNotTrigger = false;
  12743. }
  12744. for (var nodeId in this.selectionObj.nodes) {
  12745. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12746. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  12747. this.selectionObj.nodes[nodeId].unselect();
  12748. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  12749. }
  12750. }
  12751. }
  12752. if (doNotTrigger == false) {
  12753. this.emit('select', this.getSelection());
  12754. }
  12755. },
  12756. /**
  12757. * return the number of selected nodes
  12758. *
  12759. * @returns {number}
  12760. * @private
  12761. */
  12762. _getSelectedNodeCount : function() {
  12763. var count = 0;
  12764. for (var nodeId in this.selectionObj.nodes) {
  12765. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12766. count += 1;
  12767. }
  12768. }
  12769. return count;
  12770. },
  12771. /**
  12772. * return the number of selected nodes
  12773. *
  12774. * @returns {number}
  12775. * @private
  12776. */
  12777. _getSelectedNode : function() {
  12778. for (var nodeId in this.selectionObj.nodes) {
  12779. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12780. return this.selectionObj.nodes[nodeId];
  12781. }
  12782. }
  12783. return null;
  12784. },
  12785. /**
  12786. * return the number of selected edges
  12787. *
  12788. * @returns {number}
  12789. * @private
  12790. */
  12791. _getSelectedEdgeCount : function() {
  12792. var count = 0;
  12793. for (var edgeId in this.selectionObj.edges) {
  12794. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12795. count += 1;
  12796. }
  12797. }
  12798. return count;
  12799. },
  12800. /**
  12801. * return the number of selected objects.
  12802. *
  12803. * @returns {number}
  12804. * @private
  12805. */
  12806. _getSelectedObjectCount : function() {
  12807. var count = 0;
  12808. for(var nodeId in this.selectionObj.nodes) {
  12809. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12810. count += 1;
  12811. }
  12812. }
  12813. for(var edgeId in this.selectionObj.edges) {
  12814. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12815. count += 1;
  12816. }
  12817. }
  12818. return count;
  12819. },
  12820. /**
  12821. * Check if anything is selected
  12822. *
  12823. * @returns {boolean}
  12824. * @private
  12825. */
  12826. _selectionIsEmpty : function() {
  12827. for(var nodeId in this.selectionObj.nodes) {
  12828. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12829. return false;
  12830. }
  12831. }
  12832. for(var edgeId in this.selectionObj.edges) {
  12833. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12834. return false;
  12835. }
  12836. }
  12837. return true;
  12838. },
  12839. /**
  12840. * check if one of the selected nodes is a cluster.
  12841. *
  12842. * @returns {boolean}
  12843. * @private
  12844. */
  12845. _clusterInSelection : function() {
  12846. for(var nodeId in this.selectionObj.nodes) {
  12847. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12848. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  12849. return true;
  12850. }
  12851. }
  12852. }
  12853. return false;
  12854. },
  12855. /**
  12856. * select the edges connected to the node that is being selected
  12857. *
  12858. * @param {Node} node
  12859. * @private
  12860. */
  12861. _selectConnectedEdges : function(node) {
  12862. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12863. var edge = node.dynamicEdges[i];
  12864. edge.select();
  12865. this._addToSelection(edge);
  12866. }
  12867. },
  12868. /**
  12869. * unselect the edges connected to the node that is being selected
  12870. *
  12871. * @param {Node} node
  12872. * @private
  12873. */
  12874. _unselectConnectedEdges : function(node) {
  12875. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12876. var edge = node.dynamicEdges[i];
  12877. edge.unselect();
  12878. this._removeFromSelection(edge);
  12879. }
  12880. },
  12881. /**
  12882. * This is called when someone clicks on a node. either select or deselect it.
  12883. * If there is an existing selection and we don't want to append to it, clear the existing selection
  12884. *
  12885. * @param {Node || Edge} object
  12886. * @param {Boolean} append
  12887. * @param {Boolean} [doNotTrigger] | ignore trigger
  12888. * @private
  12889. */
  12890. _selectObject : function(object, append, doNotTrigger) {
  12891. if (doNotTrigger === undefined) {
  12892. doNotTrigger = false;
  12893. }
  12894. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  12895. this._unselectAll(true);
  12896. }
  12897. if (object.selected == false) {
  12898. object.select();
  12899. this._addToSelection(object);
  12900. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  12901. this._selectConnectedEdges(object);
  12902. }
  12903. }
  12904. else {
  12905. object.unselect();
  12906. this._removeFromSelection(object);
  12907. }
  12908. if (doNotTrigger == false) {
  12909. this.emit('select', this.getSelection());
  12910. }
  12911. },
  12912. /**
  12913. * handles the selection part of the touch, only for navigation controls elements;
  12914. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  12915. * This is the most responsive solution
  12916. *
  12917. * @param {Object} pointer
  12918. * @private
  12919. */
  12920. _handleTouch : function(pointer) {
  12921. },
  12922. /**
  12923. * handles the selection part of the tap;
  12924. *
  12925. * @param {Object} pointer
  12926. * @private
  12927. */
  12928. _handleTap : function(pointer) {
  12929. var node = this._getNodeAt(pointer);
  12930. if (node != null) {
  12931. this._selectObject(node,false);
  12932. }
  12933. else {
  12934. var edge = this._getEdgeAt(pointer);
  12935. if (edge != null) {
  12936. this._selectObject(edge,false);
  12937. }
  12938. else {
  12939. this._unselectAll();
  12940. }
  12941. }
  12942. this.emit("click", this.getSelection());
  12943. this._redraw();
  12944. },
  12945. /**
  12946. * handles the selection part of the double tap and opens a cluster if needed
  12947. *
  12948. * @param {Object} pointer
  12949. * @private
  12950. */
  12951. _handleDoubleTap : function(pointer) {
  12952. var node = this._getNodeAt(pointer);
  12953. if (node != null && node !== undefined) {
  12954. // we reset the areaCenter here so the opening of the node will occur
  12955. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  12956. "y" : this._canvasToY(pointer.y)};
  12957. this.openCluster(node);
  12958. }
  12959. this.emit("doubleClick", this.getSelection());
  12960. },
  12961. /**
  12962. * Handle the onHold selection part
  12963. *
  12964. * @param pointer
  12965. * @private
  12966. */
  12967. _handleOnHold : function(pointer) {
  12968. var node = this._getNodeAt(pointer);
  12969. if (node != null) {
  12970. this._selectObject(node,true);
  12971. }
  12972. else {
  12973. var edge = this._getEdgeAt(pointer);
  12974. if (edge != null) {
  12975. this._selectObject(edge,true);
  12976. }
  12977. }
  12978. this._redraw();
  12979. },
  12980. /**
  12981. * handle the onRelease event. These functions are here for the navigation controls module.
  12982. *
  12983. * @private
  12984. */
  12985. _handleOnRelease : function(pointer) {
  12986. },
  12987. /**
  12988. *
  12989. * retrieve the currently selected objects
  12990. * @return {Number[] | String[]} selection An array with the ids of the
  12991. * selected nodes.
  12992. */
  12993. getSelection : function() {
  12994. var nodeIds = this.getSelectedNodes();
  12995. var edgeIds = this.getSelectedEdges();
  12996. return {nodes:nodeIds, edges:edgeIds};
  12997. },
  12998. /**
  12999. *
  13000. * retrieve the currently selected nodes
  13001. * @return {String} selection An array with the ids of the
  13002. * selected nodes.
  13003. */
  13004. getSelectedNodes : function() {
  13005. var idArray = [];
  13006. for(var nodeId in this.selectionObj.nodes) {
  13007. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13008. idArray.push(nodeId);
  13009. }
  13010. }
  13011. return idArray
  13012. },
  13013. /**
  13014. *
  13015. * retrieve the currently selected edges
  13016. * @return {Array} selection An array with the ids of the
  13017. * selected nodes.
  13018. */
  13019. getSelectedEdges : function() {
  13020. var idArray = [];
  13021. for(var edgeId in this.selectionObj.edges) {
  13022. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13023. idArray.push(edgeId);
  13024. }
  13025. }
  13026. return idArray;
  13027. },
  13028. /**
  13029. * select zero or more nodes
  13030. * @param {Number[] | String[]} selection An array with the ids of the
  13031. * selected nodes.
  13032. */
  13033. setSelection : function(selection) {
  13034. var i, iMax, id;
  13035. if (!selection || (selection.length == undefined))
  13036. throw 'Selection must be an array with ids';
  13037. // first unselect any selected node
  13038. this._unselectAll(true);
  13039. for (i = 0, iMax = selection.length; i < iMax; i++) {
  13040. id = selection[i];
  13041. var node = this.nodes[id];
  13042. if (!node) {
  13043. throw new RangeError('Node with id "' + id + '" not found');
  13044. }
  13045. this._selectObject(node,true,true);
  13046. }
  13047. this.redraw();
  13048. },
  13049. /**
  13050. * Validate the selection: remove ids of nodes which no longer exist
  13051. * @private
  13052. */
  13053. _updateSelection : function () {
  13054. for(var nodeId in this.selectionObj.nodes) {
  13055. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13056. if (!this.nodes.hasOwnProperty(nodeId)) {
  13057. delete this.selectionObj.nodes[nodeId];
  13058. }
  13059. }
  13060. }
  13061. for(var edgeId in this.selectionObj.edges) {
  13062. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13063. if (!this.edges.hasOwnProperty(edgeId)) {
  13064. delete this.selectionObj.edges[edgeId];
  13065. }
  13066. }
  13067. }
  13068. }
  13069. };
  13070. /**
  13071. * Created by Alex on 1/22/14.
  13072. */
  13073. var NavigationMixin = {
  13074. _cleanNavigation : function() {
  13075. // clean up previosu navigation items
  13076. var wrapper = document.getElementById('graph-navigation_wrapper');
  13077. if (wrapper != null) {
  13078. this.containerElement.removeChild(wrapper);
  13079. }
  13080. document.onmouseup = null;
  13081. },
  13082. /**
  13083. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  13084. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  13085. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  13086. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  13087. *
  13088. * @private
  13089. */
  13090. _loadNavigationElements : function() {
  13091. this._cleanNavigation();
  13092. this.navigationDivs = {};
  13093. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  13094. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  13095. this.navigationDivs['wrapper'] = document.createElement('div');
  13096. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  13097. this.navigationDivs['wrapper'].style.position = "absolute";
  13098. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  13099. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  13100. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  13101. for (var i = 0; i < navigationDivs.length; i++) {
  13102. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  13103. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  13104. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  13105. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  13106. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  13107. }
  13108. document.onmouseup = this._stopMovement.bind(this);
  13109. },
  13110. /**
  13111. * this stops all movement induced by the navigation buttons
  13112. *
  13113. * @private
  13114. */
  13115. _stopMovement : function() {
  13116. this._xStopMoving();
  13117. this._yStopMoving();
  13118. this._stopZoom();
  13119. },
  13120. /**
  13121. * stops the actions performed by page up and down etc.
  13122. *
  13123. * @param event
  13124. * @private
  13125. */
  13126. _preventDefault : function(event) {
  13127. if (event !== undefined) {
  13128. if (event.preventDefault) {
  13129. event.preventDefault();
  13130. } else {
  13131. event.returnValue = false;
  13132. }
  13133. }
  13134. },
  13135. /**
  13136. * move the screen up
  13137. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  13138. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  13139. * To avoid this behaviour, we do the translation in the start loop.
  13140. *
  13141. * @private
  13142. */
  13143. _moveUp : function(event) {
  13144. this.yIncrement = this.constants.keyboard.speed.y;
  13145. this.start(); // if there is no node movement, the calculation wont be done
  13146. this._preventDefault(event);
  13147. if (this.navigationDivs) {
  13148. this.navigationDivs['up'].className += " active";
  13149. }
  13150. },
  13151. /**
  13152. * move the screen down
  13153. * @private
  13154. */
  13155. _moveDown : function(event) {
  13156. this.yIncrement = -this.constants.keyboard.speed.y;
  13157. this.start(); // if there is no node movement, the calculation wont be done
  13158. this._preventDefault(event);
  13159. if (this.navigationDivs) {
  13160. this.navigationDivs['down'].className += " active";
  13161. }
  13162. },
  13163. /**
  13164. * move the screen left
  13165. * @private
  13166. */
  13167. _moveLeft : function(event) {
  13168. this.xIncrement = this.constants.keyboard.speed.x;
  13169. this.start(); // if there is no node movement, the calculation wont be done
  13170. this._preventDefault(event);
  13171. if (this.navigationDivs) {
  13172. this.navigationDivs['left'].className += " active";
  13173. }
  13174. },
  13175. /**
  13176. * move the screen right
  13177. * @private
  13178. */
  13179. _moveRight : function(event) {
  13180. this.xIncrement = -this.constants.keyboard.speed.y;
  13181. this.start(); // if there is no node movement, the calculation wont be done
  13182. this._preventDefault(event);
  13183. if (this.navigationDivs) {
  13184. this.navigationDivs['right'].className += " active";
  13185. }
  13186. },
  13187. /**
  13188. * Zoom in, using the same method as the movement.
  13189. * @private
  13190. */
  13191. _zoomIn : function(event) {
  13192. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  13193. this.start(); // if there is no node movement, the calculation wont be done
  13194. this._preventDefault(event);
  13195. if (this.navigationDivs) {
  13196. this.navigationDivs['zoomIn'].className += " active";
  13197. }
  13198. },
  13199. /**
  13200. * Zoom out
  13201. * @private
  13202. */
  13203. _zoomOut : function() {
  13204. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  13205. this.start(); // if there is no node movement, the calculation wont be done
  13206. this._preventDefault(event);
  13207. if (this.navigationDivs) {
  13208. this.navigationDivs['zoomOut'].className += " active";
  13209. }
  13210. },
  13211. /**
  13212. * Stop zooming and unhighlight the zoom controls
  13213. * @private
  13214. */
  13215. _stopZoom : function() {
  13216. this.zoomIncrement = 0;
  13217. if (this.navigationDivs) {
  13218. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  13219. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  13220. }
  13221. },
  13222. /**
  13223. * Stop moving in the Y direction and unHighlight the up and down
  13224. * @private
  13225. */
  13226. _yStopMoving : function() {
  13227. this.yIncrement = 0;
  13228. if (this.navigationDivs) {
  13229. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  13230. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  13231. }
  13232. },
  13233. /**
  13234. * Stop moving in the X direction and unHighlight left and right.
  13235. * @private
  13236. */
  13237. _xStopMoving : function() {
  13238. this.xIncrement = 0;
  13239. if (this.navigationDivs) {
  13240. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  13241. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  13242. }
  13243. }
  13244. };
  13245. /**
  13246. * Created by Alex on 2/10/14.
  13247. */
  13248. var graphMixinLoaders = {
  13249. /**
  13250. * Load a mixin into the graph object
  13251. *
  13252. * @param {Object} sourceVariable | this object has to contain functions.
  13253. * @private
  13254. */
  13255. _loadMixin: function (sourceVariable) {
  13256. for (var mixinFunction in sourceVariable) {
  13257. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13258. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  13259. }
  13260. }
  13261. },
  13262. /**
  13263. * removes a mixin from the graph object.
  13264. *
  13265. * @param {Object} sourceVariable | this object has to contain functions.
  13266. * @private
  13267. */
  13268. _clearMixin: function (sourceVariable) {
  13269. for (var mixinFunction in sourceVariable) {
  13270. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13271. Graph.prototype[mixinFunction] = undefined;
  13272. }
  13273. }
  13274. },
  13275. /**
  13276. * Mixin the physics system and initialize the parameters required.
  13277. *
  13278. * @private
  13279. */
  13280. _loadPhysicsSystem: function () {
  13281. this._loadMixin(physicsMixin);
  13282. this._loadSelectedForceSolver();
  13283. if (this.constants.configurePhysics == true) {
  13284. this._loadPhysicsConfiguration();
  13285. }
  13286. },
  13287. /**
  13288. * Mixin the cluster system and initialize the parameters required.
  13289. *
  13290. * @private
  13291. */
  13292. _loadClusterSystem: function () {
  13293. this.clusterSession = 0;
  13294. this.hubThreshold = 5;
  13295. this._loadMixin(ClusterMixin);
  13296. },
  13297. /**
  13298. * Mixin the sector system and initialize the parameters required
  13299. *
  13300. * @private
  13301. */
  13302. _loadSectorSystem: function () {
  13303. this.sectors = { },
  13304. this.activeSector = ["default"];
  13305. this.sectors["active"] = { },
  13306. this.sectors["active"]["default"] = {"nodes": {},
  13307. "edges": {},
  13308. "nodeIndices": [],
  13309. "formationScale": 1.0,
  13310. "drawingNode": undefined };
  13311. this.sectors["frozen"] = {},
  13312. this.sectors["support"] = {"nodes": {},
  13313. "edges": {},
  13314. "nodeIndices": [],
  13315. "formationScale": 1.0,
  13316. "drawingNode": undefined };
  13317. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13318. this._loadMixin(SectorMixin);
  13319. },
  13320. /**
  13321. * Mixin the selection system and initialize the parameters required
  13322. *
  13323. * @private
  13324. */
  13325. _loadSelectionSystem: function () {
  13326. this.selectionObj = {nodes: {}, edges: {}};
  13327. this._loadMixin(SelectionMixin);
  13328. },
  13329. /**
  13330. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  13331. *
  13332. * @private
  13333. */
  13334. _loadManipulationSystem: function () {
  13335. // reset global variables -- these are used by the selection of nodes and edges.
  13336. this.blockConnectingEdgeSelection = false;
  13337. this.forceAppendSelection = false
  13338. if (this.constants.dataManipulation.enabled == true) {
  13339. // load the manipulator HTML elements. All styling done in css.
  13340. if (this.manipulationDiv === undefined) {
  13341. this.manipulationDiv = document.createElement('div');
  13342. this.manipulationDiv.className = 'graph-manipulationDiv';
  13343. this.manipulationDiv.id = 'graph-manipulationDiv';
  13344. if (this.editMode == true) {
  13345. this.manipulationDiv.style.display = "block";
  13346. }
  13347. else {
  13348. this.manipulationDiv.style.display = "none";
  13349. }
  13350. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  13351. }
  13352. if (this.editModeDiv === undefined) {
  13353. this.editModeDiv = document.createElement('div');
  13354. this.editModeDiv.className = 'graph-manipulation-editMode';
  13355. this.editModeDiv.id = 'graph-manipulation-editMode';
  13356. if (this.editMode == true) {
  13357. this.editModeDiv.style.display = "none";
  13358. }
  13359. else {
  13360. this.editModeDiv.style.display = "block";
  13361. }
  13362. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  13363. }
  13364. if (this.closeDiv === undefined) {
  13365. this.closeDiv = document.createElement('div');
  13366. this.closeDiv.className = 'graph-manipulation-closeDiv';
  13367. this.closeDiv.id = 'graph-manipulation-closeDiv';
  13368. this.closeDiv.style.display = this.manipulationDiv.style.display;
  13369. this.containerElement.insertBefore(this.closeDiv, this.frame);
  13370. }
  13371. // load the manipulation functions
  13372. this._loadMixin(manipulationMixin);
  13373. // create the manipulator toolbar
  13374. this._createManipulatorBar();
  13375. }
  13376. else {
  13377. if (this.manipulationDiv !== undefined) {
  13378. // removes all the bindings and overloads
  13379. this._createManipulatorBar();
  13380. // remove the manipulation divs
  13381. this.containerElement.removeChild(this.manipulationDiv);
  13382. this.containerElement.removeChild(this.editModeDiv);
  13383. this.containerElement.removeChild(this.closeDiv);
  13384. this.manipulationDiv = undefined;
  13385. this.editModeDiv = undefined;
  13386. this.closeDiv = undefined;
  13387. // remove the mixin functions
  13388. this._clearMixin(manipulationMixin);
  13389. }
  13390. }
  13391. },
  13392. /**
  13393. * Mixin the navigation (User Interface) system and initialize the parameters required
  13394. *
  13395. * @private
  13396. */
  13397. _loadNavigationControls: function () {
  13398. this._loadMixin(NavigationMixin);
  13399. // the clean function removes the button divs, this is done to remove the bindings.
  13400. this._cleanNavigation();
  13401. if (this.constants.navigation.enabled == true) {
  13402. this._loadNavigationElements();
  13403. }
  13404. },
  13405. /**
  13406. * Mixin the hierarchical layout system.
  13407. *
  13408. * @private
  13409. */
  13410. _loadHierarchySystem: function () {
  13411. this._loadMixin(HierarchicalLayoutMixin);
  13412. }
  13413. };
  13414. /**
  13415. * @constructor Graph
  13416. * Create a graph visualization, displaying nodes and edges.
  13417. *
  13418. * @param {Element} container The DOM element in which the Graph will
  13419. * be created. Normally a div element.
  13420. * @param {Object} data An object containing parameters
  13421. * {Array} nodes
  13422. * {Array} edges
  13423. * @param {Object} options Options
  13424. */
  13425. function Graph (container, data, options) {
  13426. this._initializeMixinLoaders();
  13427. // create variables and set default values
  13428. this.containerElement = container;
  13429. this.width = '100%';
  13430. this.height = '100%';
  13431. // render and calculation settings
  13432. this.renderRefreshRate = 60; // hz (fps)
  13433. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  13434. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  13435. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  13436. this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
  13437. this.stabilize = true; // stabilize before displaying the graph
  13438. this.selectable = true;
  13439. this.initializing = true;
  13440. // these functions are triggered when the dataset is edited
  13441. this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
  13442. // set constant values
  13443. this.constants = {
  13444. nodes: {
  13445. radiusMin: 5,
  13446. radiusMax: 20,
  13447. radius: 5,
  13448. shape: 'ellipse',
  13449. image: undefined,
  13450. widthMin: 16, // px
  13451. widthMax: 64, // px
  13452. fixed: false,
  13453. fontColor: 'black',
  13454. fontSize: 14, // px
  13455. fontFace: 'verdana',
  13456. level: -1,
  13457. color: {
  13458. border: '#2B7CE9',
  13459. background: '#97C2FC',
  13460. highlight: {
  13461. border: '#2B7CE9',
  13462. background: '#D2E5FF'
  13463. }
  13464. },
  13465. borderColor: '#2B7CE9',
  13466. backgroundColor: '#97C2FC',
  13467. highlightColor: '#D2E5FF',
  13468. group: undefined
  13469. },
  13470. edges: {
  13471. widthMin: 1,
  13472. widthMax: 15,
  13473. width: 1,
  13474. style: 'line',
  13475. color: {
  13476. color:'#848484',
  13477. highlight:'#848484'
  13478. },
  13479. fontColor: '#343434',
  13480. fontSize: 14, // px
  13481. fontFace: 'arial',
  13482. fontFill: 'white',
  13483. dash: {
  13484. length: 10,
  13485. gap: 5,
  13486. altLength: undefined
  13487. }
  13488. },
  13489. configurePhysics:false,
  13490. physics: {
  13491. barnesHut: {
  13492. enabled: true,
  13493. theta: 1 / 0.6, // inverted to save time during calculation
  13494. gravitationalConstant: -2000,
  13495. centralGravity: 0.3,
  13496. springLength: 95,
  13497. springConstant: 0.04,
  13498. damping: 0.09
  13499. },
  13500. repulsion: {
  13501. centralGravity: 0.1,
  13502. springLength: 200,
  13503. springConstant: 0.05,
  13504. nodeDistance: 100,
  13505. damping: 0.09
  13506. },
  13507. hierarchicalRepulsion: {
  13508. enabled: false,
  13509. centralGravity: 0.0,
  13510. springLength: 100,
  13511. springConstant: 0.01,
  13512. nodeDistance: 60,
  13513. damping: 0.09
  13514. },
  13515. damping: null,
  13516. centralGravity: null,
  13517. springLength: null,
  13518. springConstant: null
  13519. },
  13520. clustering: { // Per Node in Cluster = PNiC
  13521. enabled: false, // (Boolean) | global on/off switch for clustering.
  13522. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  13523. 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
  13524. 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
  13525. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  13526. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  13527. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  13528. 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.
  13529. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  13530. maxFontSize: 1000,
  13531. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  13532. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  13533. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  13534. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  13535. height: 1, // (px PNiC) | growth of the height per node in cluster.
  13536. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  13537. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  13538. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  13539. clusterLevelDifference: 2
  13540. },
  13541. navigation: {
  13542. enabled: false
  13543. },
  13544. keyboard: {
  13545. enabled: false,
  13546. speed: {x: 10, y: 10, zoom: 0.02}
  13547. },
  13548. dataManipulation: {
  13549. enabled: false,
  13550. initiallyVisible: false
  13551. },
  13552. hierarchicalLayout: {
  13553. enabled:false,
  13554. levelSeparation: 150,
  13555. nodeSpacing: 100,
  13556. direction: "UD" // UD, DU, LR, RL
  13557. },
  13558. freezeForStabilization: false,
  13559. smoothCurves: true,
  13560. maxVelocity: 10,
  13561. minVelocity: 0.1, // px/s
  13562. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  13563. labels:{
  13564. add:"Add Node",
  13565. edit:"Edit",
  13566. link:"Add Link",
  13567. del:"Delete selected",
  13568. editNode:"Edit Node",
  13569. back:"Back",
  13570. addDescription:"Click in an empty space to place a new node.",
  13571. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  13572. addError:"The function for add does not support two arguments (data,callback).",
  13573. linkError:"The function for connect does not support two arguments (data,callback).",
  13574. editError:"The function for edit does not support two arguments (data, callback).",
  13575. editBoundError:"No edit function has been bound to this button.",
  13576. deleteError:"The function for delete does not support two arguments (data, callback).",
  13577. deleteClusterError:"Clusters cannot be deleted."
  13578. },
  13579. tooltip: {
  13580. delay: 300,
  13581. fontColor: 'black',
  13582. fontSize: 14, // px
  13583. fontFace: 'verdana',
  13584. color: {
  13585. border: '#666',
  13586. background: '#FFFFC6'
  13587. }
  13588. }
  13589. };
  13590. this.editMode = this.constants.dataManipulation.initiallyVisible;
  13591. // Node variables
  13592. var graph = this;
  13593. this.groups = new Groups(); // object with groups
  13594. this.images = new Images(); // object with images
  13595. this.images.setOnloadCallback(function () {
  13596. graph._redraw();
  13597. });
  13598. // keyboard navigation variables
  13599. this.xIncrement = 0;
  13600. this.yIncrement = 0;
  13601. this.zoomIncrement = 0;
  13602. // loading all the mixins:
  13603. // load the force calculation functions, grouped under the physics system.
  13604. this._loadPhysicsSystem();
  13605. // create a frame and canvas
  13606. this._create();
  13607. // load the sector system. (mandatory, fully integrated with Graph)
  13608. this._loadSectorSystem();
  13609. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  13610. this._loadClusterSystem();
  13611. // load the selection system. (mandatory, required by Graph)
  13612. this._loadSelectionSystem();
  13613. // load the selection system. (mandatory, required by Graph)
  13614. this._loadHierarchySystem();
  13615. // apply options
  13616. this.setOptions(options);
  13617. // other vars
  13618. this.freezeSimulation = false;// freeze the simulation
  13619. this.cachedFunctions = {};
  13620. // containers for nodes and edges
  13621. this.calculationNodes = {};
  13622. this.calculationNodeIndices = [];
  13623. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  13624. this.nodes = {}; // object with Node objects
  13625. this.edges = {}; // object with Edge objects
  13626. // position and scale variables and objects
  13627. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  13628. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  13629. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  13630. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  13631. this.scale = 1; // defining the global scale variable in the constructor
  13632. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  13633. // datasets or dataviews
  13634. this.nodesData = null; // A DataSet or DataView
  13635. this.edgesData = null; // A DataSet or DataView
  13636. // create event listeners used to subscribe on the DataSets of the nodes and edges
  13637. this.nodesListeners = {
  13638. 'add': function (event, params) {
  13639. graph._addNodes(params.items);
  13640. graph.start();
  13641. },
  13642. 'update': function (event, params) {
  13643. graph._updateNodes(params.items);
  13644. graph.start();
  13645. },
  13646. 'remove': function (event, params) {
  13647. graph._removeNodes(params.items);
  13648. graph.start();
  13649. }
  13650. };
  13651. this.edgesListeners = {
  13652. 'add': function (event, params) {
  13653. graph._addEdges(params.items);
  13654. graph.start();
  13655. },
  13656. 'update': function (event, params) {
  13657. graph._updateEdges(params.items);
  13658. graph.start();
  13659. },
  13660. 'remove': function (event, params) {
  13661. graph._removeEdges(params.items);
  13662. graph.start();
  13663. }
  13664. };
  13665. // properties for the animation
  13666. this.moving = true;
  13667. this.timer = undefined; // Scheduling function. Is definded in this.start();
  13668. // load data (the disable start variable will be the same as the enabled clustering)
  13669. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  13670. // hierarchical layout
  13671. this.initializing = false;
  13672. if (this.constants.hierarchicalLayout.enabled == true) {
  13673. this._setupHierarchicalLayout();
  13674. }
  13675. else {
  13676. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  13677. if (this.stabilize == false) {
  13678. this.zoomExtent(true,this.constants.clustering.enabled);
  13679. }
  13680. }
  13681. // if clustering is disabled, the simulation will have started in the setData function
  13682. if (this.constants.clustering.enabled) {
  13683. this.startWithClustering();
  13684. }
  13685. }
  13686. // Extend Graph with an Emitter mixin
  13687. Emitter(Graph.prototype);
  13688. /**
  13689. * Get the script path where the vis.js library is located
  13690. *
  13691. * @returns {string | null} path Path or null when not found. Path does not
  13692. * end with a slash.
  13693. * @private
  13694. */
  13695. Graph.prototype._getScriptPath = function() {
  13696. var scripts = document.getElementsByTagName( 'script' );
  13697. // find script named vis.js or vis.min.js
  13698. for (var i = 0; i < scripts.length; i++) {
  13699. var src = scripts[i].src;
  13700. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  13701. if (match) {
  13702. // return path without the script name
  13703. return src.substring(0, src.length - match[0].length);
  13704. }
  13705. }
  13706. return null;
  13707. };
  13708. /**
  13709. * Find the center position of the graph
  13710. * @private
  13711. */
  13712. Graph.prototype._getRange = function() {
  13713. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  13714. for (var nodeId in this.nodes) {
  13715. if (this.nodes.hasOwnProperty(nodeId)) {
  13716. node = this.nodes[nodeId];
  13717. if (minX > (node.x)) {minX = node.x;}
  13718. if (maxX < (node.x)) {maxX = node.x;}
  13719. if (minY > (node.y)) {minY = node.y;}
  13720. if (maxY < (node.y)) {maxY = node.y;}
  13721. }
  13722. }
  13723. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  13724. minY = 0, maxY = 0, minX = 0, maxX = 0;
  13725. }
  13726. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13727. };
  13728. /**
  13729. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13730. * @returns {{x: number, y: number}}
  13731. * @private
  13732. */
  13733. Graph.prototype._findCenter = function(range) {
  13734. return {x: (0.5 * (range.maxX + range.minX)),
  13735. y: (0.5 * (range.maxY + range.minY))};
  13736. };
  13737. /**
  13738. * center the graph
  13739. *
  13740. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13741. */
  13742. Graph.prototype._centerGraph = function(range) {
  13743. var center = this._findCenter(range);
  13744. center.x *= this.scale;
  13745. center.y *= this.scale;
  13746. center.x -= 0.5 * this.frame.canvas.clientWidth;
  13747. center.y -= 0.5 * this.frame.canvas.clientHeight;
  13748. this._setTranslation(-center.x,-center.y); // set at 0,0
  13749. };
  13750. /**
  13751. * This function zooms out to fit all data on screen based on amount of nodes
  13752. *
  13753. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  13754. */
  13755. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  13756. if (initialZoom === undefined) {
  13757. initialZoom = false;
  13758. }
  13759. if (disableStart === undefined) {
  13760. disableStart = false;
  13761. }
  13762. var range = this._getRange();
  13763. var zoomLevel;
  13764. if (initialZoom == true) {
  13765. var numberOfNodes = this.nodeIndices.length;
  13766. if (this.constants.smoothCurves == true) {
  13767. if (this.constants.clustering.enabled == true &&
  13768. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  13769. 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.
  13770. }
  13771. else {
  13772. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  13773. }
  13774. }
  13775. else {
  13776. if (this.constants.clustering.enabled == true &&
  13777. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  13778. 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.
  13779. }
  13780. else {
  13781. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  13782. }
  13783. }
  13784. // correct for larger canvasses.
  13785. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  13786. zoomLevel *= factor;
  13787. }
  13788. else {
  13789. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  13790. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  13791. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  13792. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  13793. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  13794. }
  13795. if (zoomLevel > 1.0) {
  13796. zoomLevel = 1.0;
  13797. }
  13798. this._setScale(zoomLevel);
  13799. this._centerGraph(range);
  13800. if (disableStart == false) {
  13801. this.moving = true;
  13802. this.start();
  13803. }
  13804. };
  13805. /**
  13806. * Update the this.nodeIndices with the most recent node index list
  13807. * @private
  13808. */
  13809. Graph.prototype._updateNodeIndexList = function() {
  13810. this._clearNodeIndexList();
  13811. for (var idx in this.nodes) {
  13812. if (this.nodes.hasOwnProperty(idx)) {
  13813. this.nodeIndices.push(idx);
  13814. }
  13815. }
  13816. };
  13817. /**
  13818. * Set nodes and edges, and optionally options as well.
  13819. *
  13820. * @param {Object} data Object containing parameters:
  13821. * {Array | DataSet | DataView} [nodes] Array with nodes
  13822. * {Array | DataSet | DataView} [edges] Array with edges
  13823. * {String} [dot] String containing data in DOT format
  13824. * {Options} [options] Object with options
  13825. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  13826. */
  13827. Graph.prototype.setData = function(data, disableStart) {
  13828. if (disableStart === undefined) {
  13829. disableStart = false;
  13830. }
  13831. if (data && data.dot && (data.nodes || data.edges)) {
  13832. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  13833. ' parameter pair "nodes" and "edges", but not both.');
  13834. }
  13835. // set options
  13836. this.setOptions(data && data.options);
  13837. // set all data
  13838. if (data && data.dot) {
  13839. // parse DOT file
  13840. if(data && data.dot) {
  13841. var dotData = vis.util.DOTToGraph(data.dot);
  13842. this.setData(dotData);
  13843. return;
  13844. }
  13845. }
  13846. else {
  13847. this._setNodes(data && data.nodes);
  13848. this._setEdges(data && data.edges);
  13849. }
  13850. this._putDataInSector();
  13851. if (!disableStart) {
  13852. // find a stable position or start animating to a stable position
  13853. if (this.stabilize) {
  13854. this._stabilize();
  13855. }
  13856. this.start();
  13857. }
  13858. };
  13859. /**
  13860. * Set options
  13861. * @param {Object} options
  13862. */
  13863. Graph.prototype.setOptions = function (options) {
  13864. if (options) {
  13865. var prop;
  13866. // retrieve parameter values
  13867. if (options.width !== undefined) {this.width = options.width;}
  13868. if (options.height !== undefined) {this.height = options.height;}
  13869. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  13870. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  13871. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  13872. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  13873. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  13874. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  13875. if (options.labels !== undefined) {
  13876. for (prop in options.labels) {
  13877. if (options.labels.hasOwnProperty(prop)) {
  13878. this.constants.labels[prop] = options.labels[prop];
  13879. }
  13880. }
  13881. }
  13882. if (options.onAdd) {
  13883. this.triggerFunctions.add = options.onAdd;
  13884. }
  13885. if (options.onEdit) {
  13886. this.triggerFunctions.edit = options.onEdit;
  13887. }
  13888. if (options.onConnect) {
  13889. this.triggerFunctions.connect = options.onConnect;
  13890. }
  13891. if (options.onDelete) {
  13892. this.triggerFunctions.del = options.onDelete;
  13893. }
  13894. if (options.physics) {
  13895. if (options.physics.barnesHut) {
  13896. this.constants.physics.barnesHut.enabled = true;
  13897. for (prop in options.physics.barnesHut) {
  13898. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  13899. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  13900. }
  13901. }
  13902. }
  13903. if (options.physics.repulsion) {
  13904. this.constants.physics.barnesHut.enabled = false;
  13905. for (prop in options.physics.repulsion) {
  13906. if (options.physics.repulsion.hasOwnProperty(prop)) {
  13907. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  13908. }
  13909. }
  13910. }
  13911. }
  13912. if (options.hierarchicalLayout) {
  13913. this.constants.hierarchicalLayout.enabled = true;
  13914. for (prop in options.hierarchicalLayout) {
  13915. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  13916. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  13917. }
  13918. }
  13919. }
  13920. else if (options.hierarchicalLayout !== undefined) {
  13921. this.constants.hierarchicalLayout.enabled = false;
  13922. }
  13923. if (options.clustering) {
  13924. this.constants.clustering.enabled = true;
  13925. for (prop in options.clustering) {
  13926. if (options.clustering.hasOwnProperty(prop)) {
  13927. this.constants.clustering[prop] = options.clustering[prop];
  13928. }
  13929. }
  13930. }
  13931. else if (options.clustering !== undefined) {
  13932. this.constants.clustering.enabled = false;
  13933. }
  13934. if (options.navigation) {
  13935. this.constants.navigation.enabled = true;
  13936. for (prop in options.navigation) {
  13937. if (options.navigation.hasOwnProperty(prop)) {
  13938. this.constants.navigation[prop] = options.navigation[prop];
  13939. }
  13940. }
  13941. }
  13942. else if (options.navigation !== undefined) {
  13943. this.constants.navigation.enabled = false;
  13944. }
  13945. if (options.keyboard) {
  13946. this.constants.keyboard.enabled = true;
  13947. for (prop in options.keyboard) {
  13948. if (options.keyboard.hasOwnProperty(prop)) {
  13949. this.constants.keyboard[prop] = options.keyboard[prop];
  13950. }
  13951. }
  13952. }
  13953. else if (options.keyboard !== undefined) {
  13954. this.constants.keyboard.enabled = false;
  13955. }
  13956. if (options.dataManipulation) {
  13957. this.constants.dataManipulation.enabled = true;
  13958. for (prop in options.dataManipulation) {
  13959. if (options.dataManipulation.hasOwnProperty(prop)) {
  13960. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  13961. }
  13962. }
  13963. }
  13964. else if (options.dataManipulation !== undefined) {
  13965. this.constants.dataManipulation.enabled = false;
  13966. }
  13967. // TODO: work out these options and document them
  13968. if (options.edges) {
  13969. for (prop in options.edges) {
  13970. if (options.edges.hasOwnProperty(prop)) {
  13971. if (typeof options.edges[prop] != "object") {
  13972. this.constants.edges[prop] = options.edges[prop];
  13973. }
  13974. }
  13975. }
  13976. if (options.edges.color !== undefined) {
  13977. if (util.isString(options.edges.color)) {
  13978. this.constants.edges.color = {};
  13979. this.constants.edges.color.color = options.edges.color;
  13980. this.constants.edges.color.highlight = options.edges.color;
  13981. }
  13982. else {
  13983. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  13984. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  13985. }
  13986. }
  13987. if (!options.edges.fontColor) {
  13988. if (options.edges.color !== undefined) {
  13989. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  13990. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  13991. }
  13992. }
  13993. // Added to support dashed lines
  13994. // David Jordan
  13995. // 2012-08-08
  13996. if (options.edges.dash) {
  13997. if (options.edges.dash.length !== undefined) {
  13998. this.constants.edges.dash.length = options.edges.dash.length;
  13999. }
  14000. if (options.edges.dash.gap !== undefined) {
  14001. this.constants.edges.dash.gap = options.edges.dash.gap;
  14002. }
  14003. if (options.edges.dash.altLength !== undefined) {
  14004. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14005. }
  14006. }
  14007. }
  14008. if (options.nodes) {
  14009. for (prop in options.nodes) {
  14010. if (options.nodes.hasOwnProperty(prop)) {
  14011. this.constants.nodes[prop] = options.nodes[prop];
  14012. }
  14013. }
  14014. if (options.nodes.color) {
  14015. this.constants.nodes.color = util.parseColor(options.nodes.color);
  14016. }
  14017. /*
  14018. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14019. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14020. */
  14021. }
  14022. if (options.groups) {
  14023. for (var groupname in options.groups) {
  14024. if (options.groups.hasOwnProperty(groupname)) {
  14025. var group = options.groups[groupname];
  14026. this.groups.add(groupname, group);
  14027. }
  14028. }
  14029. }
  14030. if (options.tooltip) {
  14031. for (prop in options.tooltip) {
  14032. if (options.tooltip.hasOwnProperty(prop)) {
  14033. this.constants.tooltip[prop] = options.tooltip[prop];
  14034. }
  14035. }
  14036. if (options.tooltip.color) {
  14037. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  14038. }
  14039. }
  14040. }
  14041. // (Re)loading the mixins that can be enabled or disabled in the options.
  14042. // load the force calculation functions, grouped under the physics system.
  14043. this._loadPhysicsSystem();
  14044. // load the navigation system.
  14045. this._loadNavigationControls();
  14046. // load the data manipulation system
  14047. this._loadManipulationSystem();
  14048. // configure the smooth curves
  14049. this._configureSmoothCurves();
  14050. // bind keys. If disabled, this will not do anything;
  14051. this._createKeyBinds();
  14052. this.setSize(this.width, this.height);
  14053. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14054. this._setScale(1);
  14055. this._redraw();
  14056. };
  14057. /**
  14058. * Create the main frame for the Graph.
  14059. * This function is executed once when a Graph object is created. The frame
  14060. * contains a canvas, and this canvas contains all objects like the axis and
  14061. * nodes.
  14062. * @private
  14063. */
  14064. Graph.prototype._create = function () {
  14065. // remove all elements from the container element.
  14066. while (this.containerElement.hasChildNodes()) {
  14067. this.containerElement.removeChild(this.containerElement.firstChild);
  14068. }
  14069. this.frame = document.createElement('div');
  14070. this.frame.className = 'graph-frame';
  14071. this.frame.style.position = 'relative';
  14072. this.frame.style.overflow = 'hidden';
  14073. // create the graph canvas (HTML canvas element)
  14074. this.frame.canvas = document.createElement( 'canvas' );
  14075. this.frame.canvas.style.position = 'relative';
  14076. this.frame.appendChild(this.frame.canvas);
  14077. if (!this.frame.canvas.getContext) {
  14078. var noCanvas = document.createElement( 'DIV' );
  14079. noCanvas.style.color = 'red';
  14080. noCanvas.style.fontWeight = 'bold' ;
  14081. noCanvas.style.padding = '10px';
  14082. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14083. this.frame.canvas.appendChild(noCanvas);
  14084. }
  14085. var me = this;
  14086. this.drag = {};
  14087. this.pinch = {};
  14088. this.hammer = Hammer(this.frame.canvas, {
  14089. prevent_default: true
  14090. });
  14091. this.hammer.on('tap', me._onTap.bind(me) );
  14092. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14093. this.hammer.on('hold', me._onHold.bind(me) );
  14094. this.hammer.on('pinch', me._onPinch.bind(me) );
  14095. this.hammer.on('touch', me._onTouch.bind(me) );
  14096. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14097. this.hammer.on('drag', me._onDrag.bind(me) );
  14098. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14099. this.hammer.on('release', me._onRelease.bind(me) );
  14100. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14101. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14102. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14103. // add the frame to the container element
  14104. this.containerElement.appendChild(this.frame);
  14105. };
  14106. /**
  14107. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14108. * @private
  14109. */
  14110. Graph.prototype._createKeyBinds = function() {
  14111. var me = this;
  14112. this.mousetrap = mousetrap;
  14113. this.mousetrap.reset();
  14114. if (this.constants.keyboard.enabled == true) {
  14115. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14116. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14117. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14118. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14119. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14120. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14121. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14122. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14123. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14124. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14125. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14126. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14127. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14128. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14129. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14130. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14131. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14132. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14133. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14134. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14135. }
  14136. if (this.constants.dataManipulation.enabled == true) {
  14137. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14138. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14139. }
  14140. };
  14141. /**
  14142. * Get the pointer location from a touch location
  14143. * @param {{pageX: Number, pageY: Number}} touch
  14144. * @return {{x: Number, y: Number}} pointer
  14145. * @private
  14146. */
  14147. Graph.prototype._getPointer = function (touch) {
  14148. return {
  14149. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14150. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14151. };
  14152. };
  14153. /**
  14154. * On start of a touch gesture, store the pointer
  14155. * @param event
  14156. * @private
  14157. */
  14158. Graph.prototype._onTouch = function (event) {
  14159. this.drag.pointer = this._getPointer(event.gesture.center);
  14160. this.drag.pinched = false;
  14161. this.pinch.scale = this._getScale();
  14162. this._handleTouch(this.drag.pointer);
  14163. };
  14164. /**
  14165. * handle drag start event
  14166. * @private
  14167. */
  14168. Graph.prototype._onDragStart = function () {
  14169. this._handleDragStart();
  14170. };
  14171. /**
  14172. * This function is called by _onDragStart.
  14173. * It is separated out because we can then overload it for the datamanipulation system.
  14174. *
  14175. * @private
  14176. */
  14177. Graph.prototype._handleDragStart = function() {
  14178. var drag = this.drag;
  14179. var node = this._getNodeAt(drag.pointer);
  14180. // note: drag.pointer is set in _onTouch to get the initial touch location
  14181. drag.dragging = true;
  14182. drag.selection = [];
  14183. drag.translation = this._getTranslation();
  14184. drag.nodeId = null;
  14185. if (node != null) {
  14186. drag.nodeId = node.id;
  14187. // select the clicked node if not yet selected
  14188. if (!node.isSelected()) {
  14189. this._selectObject(node,false);
  14190. }
  14191. // create an array with the selected nodes and their original location and status
  14192. for (var objectId in this.selectionObj.nodes) {
  14193. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  14194. var object = this.selectionObj.nodes[objectId];
  14195. var s = {
  14196. id: object.id,
  14197. node: object,
  14198. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14199. x: object.x,
  14200. y: object.y,
  14201. xFixed: object.xFixed,
  14202. yFixed: object.yFixed
  14203. };
  14204. object.xFixed = true;
  14205. object.yFixed = true;
  14206. drag.selection.push(s);
  14207. }
  14208. }
  14209. }
  14210. };
  14211. /**
  14212. * handle drag event
  14213. * @private
  14214. */
  14215. Graph.prototype._onDrag = function (event) {
  14216. this._handleOnDrag(event)
  14217. };
  14218. /**
  14219. * This function is called by _onDrag.
  14220. * It is separated out because we can then overload it for the datamanipulation system.
  14221. *
  14222. * @private
  14223. */
  14224. Graph.prototype._handleOnDrag = function(event) {
  14225. if (this.drag.pinched) {
  14226. return;
  14227. }
  14228. var pointer = this._getPointer(event.gesture.center);
  14229. var me = this,
  14230. drag = this.drag,
  14231. selection = drag.selection;
  14232. if (selection && selection.length) {
  14233. // calculate delta's and new location
  14234. var deltaX = pointer.x - drag.pointer.x,
  14235. deltaY = pointer.y - drag.pointer.y;
  14236. // update position of all selected nodes
  14237. selection.forEach(function (s) {
  14238. var node = s.node;
  14239. if (!s.xFixed) {
  14240. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  14241. }
  14242. if (!s.yFixed) {
  14243. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  14244. }
  14245. });
  14246. // start _animationStep if not yet running
  14247. if (!this.moving) {
  14248. this.moving = true;
  14249. this.start();
  14250. }
  14251. }
  14252. else {
  14253. // move the graph
  14254. var diffX = pointer.x - this.drag.pointer.x;
  14255. var diffY = pointer.y - this.drag.pointer.y;
  14256. this._setTranslation(
  14257. this.drag.translation.x + diffX,
  14258. this.drag.translation.y + diffY);
  14259. this._redraw();
  14260. this.moved = true;
  14261. }
  14262. };
  14263. /**
  14264. * handle drag start event
  14265. * @private
  14266. */
  14267. Graph.prototype._onDragEnd = function () {
  14268. this.drag.dragging = false;
  14269. var selection = this.drag.selection;
  14270. if (selection) {
  14271. selection.forEach(function (s) {
  14272. // restore original xFixed and yFixed
  14273. s.node.xFixed = s.xFixed;
  14274. s.node.yFixed = s.yFixed;
  14275. });
  14276. }
  14277. };
  14278. /**
  14279. * handle tap/click event: select/unselect a node
  14280. * @private
  14281. */
  14282. Graph.prototype._onTap = function (event) {
  14283. var pointer = this._getPointer(event.gesture.center);
  14284. this.pointerPosition = pointer;
  14285. this._handleTap(pointer);
  14286. };
  14287. /**
  14288. * handle doubletap event
  14289. * @private
  14290. */
  14291. Graph.prototype._onDoubleTap = function (event) {
  14292. var pointer = this._getPointer(event.gesture.center);
  14293. this._handleDoubleTap(pointer);
  14294. };
  14295. /**
  14296. * handle long tap event: multi select nodes
  14297. * @private
  14298. */
  14299. Graph.prototype._onHold = function (event) {
  14300. var pointer = this._getPointer(event.gesture.center);
  14301. this.pointerPosition = pointer;
  14302. this._handleOnHold(pointer);
  14303. };
  14304. /**
  14305. * handle the release of the screen
  14306. *
  14307. * @private
  14308. */
  14309. Graph.prototype._onRelease = function (event) {
  14310. var pointer = this._getPointer(event.gesture.center);
  14311. this._handleOnRelease(pointer);
  14312. };
  14313. /**
  14314. * Handle pinch event
  14315. * @param event
  14316. * @private
  14317. */
  14318. Graph.prototype._onPinch = function (event) {
  14319. var pointer = this._getPointer(event.gesture.center);
  14320. this.drag.pinched = true;
  14321. if (!('scale' in this.pinch)) {
  14322. this.pinch.scale = 1;
  14323. }
  14324. // TODO: enabled moving while pinching?
  14325. var scale = this.pinch.scale * event.gesture.scale;
  14326. this._zoom(scale, pointer)
  14327. };
  14328. /**
  14329. * Zoom the graph in or out
  14330. * @param {Number} scale a number around 1, and between 0.01 and 10
  14331. * @param {{x: Number, y: Number}} pointer Position on screen
  14332. * @return {Number} appliedScale scale is limited within the boundaries
  14333. * @private
  14334. */
  14335. Graph.prototype._zoom = function(scale, pointer) {
  14336. var scaleOld = this._getScale();
  14337. if (scale < 0.00001) {
  14338. scale = 0.00001;
  14339. }
  14340. if (scale > 10) {
  14341. scale = 10;
  14342. }
  14343. // + this.frame.canvas.clientHeight / 2
  14344. var translation = this._getTranslation();
  14345. var scaleFrac = scale / scaleOld;
  14346. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14347. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14348. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  14349. "y" : this._canvasToY(pointer.y)};
  14350. this._setScale(scale);
  14351. this._setTranslation(tx, ty);
  14352. this.updateClustersDefault();
  14353. this._redraw();
  14354. return scale;
  14355. };
  14356. /**
  14357. * Event handler for mouse wheel event, used to zoom the timeline
  14358. * See http://adomas.org/javascript-mouse-wheel/
  14359. * https://github.com/EightMedia/hammer.js/issues/256
  14360. * @param {MouseEvent} event
  14361. * @private
  14362. */
  14363. Graph.prototype._onMouseWheel = function(event) {
  14364. // retrieve delta
  14365. var delta = 0;
  14366. if (event.wheelDelta) { /* IE/Opera. */
  14367. delta = event.wheelDelta/120;
  14368. } else if (event.detail) { /* Mozilla case. */
  14369. // In Mozilla, sign of delta is different than in IE.
  14370. // Also, delta is multiple of 3.
  14371. delta = -event.detail/3;
  14372. }
  14373. // If delta is nonzero, handle it.
  14374. // Basically, delta is now positive if wheel was scrolled up,
  14375. // and negative, if wheel was scrolled down.
  14376. if (delta) {
  14377. // calculate the new scale
  14378. var scale = this._getScale();
  14379. var zoom = delta / 10;
  14380. if (delta < 0) {
  14381. zoom = zoom / (1 - zoom);
  14382. }
  14383. scale *= (1 + zoom);
  14384. // calculate the pointer location
  14385. var gesture = util.fakeGesture(this, event);
  14386. var pointer = this._getPointer(gesture.center);
  14387. // apply the new scale
  14388. this._zoom(scale, pointer);
  14389. }
  14390. // Prevent default actions caused by mouse wheel.
  14391. event.preventDefault();
  14392. };
  14393. /**
  14394. * Mouse move handler for checking whether the title moves over a node with a title.
  14395. * @param {Event} event
  14396. * @private
  14397. */
  14398. Graph.prototype._onMouseMoveTitle = function (event) {
  14399. var gesture = util.fakeGesture(this, event);
  14400. var pointer = this._getPointer(gesture.center);
  14401. // check if the previously selected node is still selected
  14402. if (this.popupNode) {
  14403. this._checkHidePopup(pointer);
  14404. }
  14405. // start a timeout that will check if the mouse is positioned above
  14406. // an element
  14407. var me = this;
  14408. var checkShow = function() {
  14409. me._checkShowPopup(pointer);
  14410. };
  14411. if (this.popupTimer) {
  14412. clearInterval(this.popupTimer); // stop any running calculationTimer
  14413. }
  14414. if (!this.drag.dragging) {
  14415. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  14416. }
  14417. };
  14418. /**
  14419. * Check if there is an element on the given position in the graph
  14420. * (a node or edge). If so, and if this element has a title,
  14421. * show a popup window with its title.
  14422. *
  14423. * @param {{x:Number, y:Number}} pointer
  14424. * @private
  14425. */
  14426. Graph.prototype._checkShowPopup = function (pointer) {
  14427. var obj = {
  14428. left: this._canvasToX(pointer.x),
  14429. top: this._canvasToY(pointer.y),
  14430. right: this._canvasToX(pointer.x),
  14431. bottom: this._canvasToY(pointer.y)
  14432. };
  14433. var id;
  14434. var lastPopupNode = this.popupNode;
  14435. if (this.popupNode == undefined) {
  14436. // search the nodes for overlap, select the top one in case of multiple nodes
  14437. var nodes = this.nodes;
  14438. for (id in nodes) {
  14439. if (nodes.hasOwnProperty(id)) {
  14440. var node = nodes[id];
  14441. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  14442. this.popupNode = node;
  14443. break;
  14444. }
  14445. }
  14446. }
  14447. }
  14448. if (this.popupNode === undefined) {
  14449. // search the edges for overlap
  14450. var edges = this.edges;
  14451. for (id in edges) {
  14452. if (edges.hasOwnProperty(id)) {
  14453. var edge = edges[id];
  14454. if (edge.connected && (edge.getTitle() !== undefined) &&
  14455. edge.isOverlappingWith(obj)) {
  14456. this.popupNode = edge;
  14457. break;
  14458. }
  14459. }
  14460. }
  14461. }
  14462. if (this.popupNode) {
  14463. // show popup message window
  14464. if (this.popupNode != lastPopupNode) {
  14465. var me = this;
  14466. if (!me.popup) {
  14467. me.popup = new Popup(me.frame, me.constants.tooltip);
  14468. }
  14469. // adjust a small offset such that the mouse cursor is located in the
  14470. // bottom left location of the popup, and you can easily move over the
  14471. // popup area
  14472. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  14473. me.popup.setText(me.popupNode.getTitle());
  14474. me.popup.show();
  14475. }
  14476. }
  14477. else {
  14478. if (this.popup) {
  14479. this.popup.hide();
  14480. }
  14481. }
  14482. };
  14483. /**
  14484. * Check if the popup must be hided, which is the case when the mouse is no
  14485. * longer hovering on the object
  14486. * @param {{x:Number, y:Number}} pointer
  14487. * @private
  14488. */
  14489. Graph.prototype._checkHidePopup = function (pointer) {
  14490. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  14491. this.popupNode = undefined;
  14492. if (this.popup) {
  14493. this.popup.hide();
  14494. }
  14495. }
  14496. };
  14497. /**
  14498. * Set a new size for the graph
  14499. * @param {string} width Width in pixels or percentage (for example '800px'
  14500. * or '50%')
  14501. * @param {string} height Height in pixels or percentage (for example '400px'
  14502. * or '30%')
  14503. */
  14504. Graph.prototype.setSize = function(width, height) {
  14505. this.frame.style.width = width;
  14506. this.frame.style.height = height;
  14507. this.frame.canvas.style.width = '100%';
  14508. this.frame.canvas.style.height = '100%';
  14509. this.frame.canvas.width = this.frame.canvas.clientWidth;
  14510. this.frame.canvas.height = this.frame.canvas.clientHeight;
  14511. if (this.manipulationDiv !== undefined) {
  14512. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  14513. }
  14514. if (this.navigationDivs !== undefined) {
  14515. if (this.navigationDivs['wrapper'] !== undefined) {
  14516. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  14517. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  14518. }
  14519. }
  14520. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  14521. };
  14522. /**
  14523. * Set a data set with nodes for the graph
  14524. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  14525. * @private
  14526. */
  14527. Graph.prototype._setNodes = function(nodes) {
  14528. var oldNodesData = this.nodesData;
  14529. if (nodes instanceof DataSet || nodes instanceof DataView) {
  14530. this.nodesData = nodes;
  14531. }
  14532. else if (nodes instanceof Array) {
  14533. this.nodesData = new DataSet();
  14534. this.nodesData.add(nodes);
  14535. }
  14536. else if (!nodes) {
  14537. this.nodesData = new DataSet();
  14538. }
  14539. else {
  14540. throw new TypeError('Array or DataSet expected');
  14541. }
  14542. if (oldNodesData) {
  14543. // unsubscribe from old dataset
  14544. util.forEach(this.nodesListeners, function (callback, event) {
  14545. oldNodesData.off(event, callback);
  14546. });
  14547. }
  14548. // remove drawn nodes
  14549. this.nodes = {};
  14550. if (this.nodesData) {
  14551. // subscribe to new dataset
  14552. var me = this;
  14553. util.forEach(this.nodesListeners, function (callback, event) {
  14554. me.nodesData.on(event, callback);
  14555. });
  14556. // draw all new nodes
  14557. var ids = this.nodesData.getIds();
  14558. this._addNodes(ids);
  14559. }
  14560. this._updateSelection();
  14561. };
  14562. /**
  14563. * Add nodes
  14564. * @param {Number[] | String[]} ids
  14565. * @private
  14566. */
  14567. Graph.prototype._addNodes = function(ids) {
  14568. var id;
  14569. for (var i = 0, len = ids.length; i < len; i++) {
  14570. id = ids[i];
  14571. var data = this.nodesData.get(id);
  14572. var node = new Node(data, this.images, this.groups, this.constants);
  14573. this.nodes[id] = node; // note: this may replace an existing node
  14574. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  14575. var radius = 10 * 0.1*ids.length;
  14576. var angle = 2 * Math.PI * Math.random();
  14577. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  14578. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  14579. }
  14580. this.moving = true;
  14581. }
  14582. this._updateNodeIndexList();
  14583. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14584. this._resetLevels();
  14585. this._setupHierarchicalLayout();
  14586. }
  14587. this._updateCalculationNodes();
  14588. this._reconnectEdges();
  14589. this._updateValueRange(this.nodes);
  14590. this.updateLabels();
  14591. };
  14592. /**
  14593. * Update existing nodes, or create them when not yet existing
  14594. * @param {Number[] | String[]} ids
  14595. * @private
  14596. */
  14597. Graph.prototype._updateNodes = function(ids) {
  14598. var nodes = this.nodes,
  14599. nodesData = this.nodesData;
  14600. for (var i = 0, len = ids.length; i < len; i++) {
  14601. var id = ids[i];
  14602. var node = nodes[id];
  14603. var data = nodesData.get(id);
  14604. if (node) {
  14605. // update node
  14606. node.setProperties(data, this.constants);
  14607. }
  14608. else {
  14609. // create node
  14610. node = new Node(properties, this.images, this.groups, this.constants);
  14611. nodes[id] = node;
  14612. }
  14613. }
  14614. this.moving = true;
  14615. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14616. this._resetLevels();
  14617. this._setupHierarchicalLayout();
  14618. }
  14619. this._updateNodeIndexList();
  14620. this._reconnectEdges();
  14621. this._updateValueRange(nodes);
  14622. };
  14623. /**
  14624. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  14625. * @param {Number[] | String[]} ids
  14626. * @private
  14627. */
  14628. Graph.prototype._removeNodes = function(ids) {
  14629. var nodes = this.nodes;
  14630. for (var i = 0, len = ids.length; i < len; i++) {
  14631. var id = ids[i];
  14632. delete nodes[id];
  14633. }
  14634. this._updateNodeIndexList();
  14635. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14636. this._resetLevels();
  14637. this._setupHierarchicalLayout();
  14638. }
  14639. this._updateCalculationNodes();
  14640. this._reconnectEdges();
  14641. this._updateSelection();
  14642. this._updateValueRange(nodes);
  14643. };
  14644. /**
  14645. * Load edges by reading the data table
  14646. * @param {Array | DataSet | DataView} edges The data containing the edges.
  14647. * @private
  14648. * @private
  14649. */
  14650. Graph.prototype._setEdges = function(edges) {
  14651. var oldEdgesData = this.edgesData;
  14652. if (edges instanceof DataSet || edges instanceof DataView) {
  14653. this.edgesData = edges;
  14654. }
  14655. else if (edges instanceof Array) {
  14656. this.edgesData = new DataSet();
  14657. this.edgesData.add(edges);
  14658. }
  14659. else if (!edges) {
  14660. this.edgesData = new DataSet();
  14661. }
  14662. else {
  14663. throw new TypeError('Array or DataSet expected');
  14664. }
  14665. if (oldEdgesData) {
  14666. // unsubscribe from old dataset
  14667. util.forEach(this.edgesListeners, function (callback, event) {
  14668. oldEdgesData.off(event, callback);
  14669. });
  14670. }
  14671. // remove drawn edges
  14672. this.edges = {};
  14673. if (this.edgesData) {
  14674. // subscribe to new dataset
  14675. var me = this;
  14676. util.forEach(this.edgesListeners, function (callback, event) {
  14677. me.edgesData.on(event, callback);
  14678. });
  14679. // draw all new nodes
  14680. var ids = this.edgesData.getIds();
  14681. this._addEdges(ids);
  14682. }
  14683. this._reconnectEdges();
  14684. };
  14685. /**
  14686. * Add edges
  14687. * @param {Number[] | String[]} ids
  14688. * @private
  14689. */
  14690. Graph.prototype._addEdges = function (ids) {
  14691. var edges = this.edges,
  14692. edgesData = this.edgesData;
  14693. for (var i = 0, len = ids.length; i < len; i++) {
  14694. var id = ids[i];
  14695. var oldEdge = edges[id];
  14696. if (oldEdge) {
  14697. oldEdge.disconnect();
  14698. }
  14699. var data = edgesData.get(id, {"showInternalIds" : true});
  14700. edges[id] = new Edge(data, this, this.constants);
  14701. }
  14702. this.moving = true;
  14703. this._updateValueRange(edges);
  14704. this._createBezierNodes();
  14705. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14706. this._resetLevels();
  14707. this._setupHierarchicalLayout();
  14708. }
  14709. this._updateCalculationNodes();
  14710. };
  14711. /**
  14712. * Update existing edges, or create them when not yet existing
  14713. * @param {Number[] | String[]} ids
  14714. * @private
  14715. */
  14716. Graph.prototype._updateEdges = function (ids) {
  14717. var edges = this.edges,
  14718. edgesData = this.edgesData;
  14719. for (var i = 0, len = ids.length; i < len; i++) {
  14720. var id = ids[i];
  14721. var data = edgesData.get(id);
  14722. var edge = edges[id];
  14723. if (edge) {
  14724. // update edge
  14725. edge.disconnect();
  14726. edge.setProperties(data, this.constants);
  14727. edge.connect();
  14728. }
  14729. else {
  14730. // create edge
  14731. edge = new Edge(data, this, this.constants);
  14732. this.edges[id] = edge;
  14733. }
  14734. }
  14735. this._createBezierNodes();
  14736. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14737. this._resetLevels();
  14738. this._setupHierarchicalLayout();
  14739. }
  14740. this.moving = true;
  14741. this._updateValueRange(edges);
  14742. };
  14743. /**
  14744. * Remove existing edges. Non existing ids will be ignored
  14745. * @param {Number[] | String[]} ids
  14746. * @private
  14747. */
  14748. Graph.prototype._removeEdges = function (ids) {
  14749. var edges = this.edges;
  14750. for (var i = 0, len = ids.length; i < len; i++) {
  14751. var id = ids[i];
  14752. var edge = edges[id];
  14753. if (edge) {
  14754. if (edge.via != null) {
  14755. delete this.sectors['support']['nodes'][edge.via.id];
  14756. }
  14757. edge.disconnect();
  14758. delete edges[id];
  14759. }
  14760. }
  14761. this.moving = true;
  14762. this._updateValueRange(edges);
  14763. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14764. this._resetLevels();
  14765. this._setupHierarchicalLayout();
  14766. }
  14767. this._updateCalculationNodes();
  14768. };
  14769. /**
  14770. * Reconnect all edges
  14771. * @private
  14772. */
  14773. Graph.prototype._reconnectEdges = function() {
  14774. var id,
  14775. nodes = this.nodes,
  14776. edges = this.edges;
  14777. for (id in nodes) {
  14778. if (nodes.hasOwnProperty(id)) {
  14779. nodes[id].edges = [];
  14780. }
  14781. }
  14782. for (id in edges) {
  14783. if (edges.hasOwnProperty(id)) {
  14784. var edge = edges[id];
  14785. edge.from = null;
  14786. edge.to = null;
  14787. edge.connect();
  14788. }
  14789. }
  14790. };
  14791. /**
  14792. * Update the values of all object in the given array according to the current
  14793. * value range of the objects in the array.
  14794. * @param {Object} obj An object containing a set of Edges or Nodes
  14795. * The objects must have a method getValue() and
  14796. * setValueRange(min, max).
  14797. * @private
  14798. */
  14799. Graph.prototype._updateValueRange = function(obj) {
  14800. var id;
  14801. // determine the range of the objects
  14802. var valueMin = undefined;
  14803. var valueMax = undefined;
  14804. for (id in obj) {
  14805. if (obj.hasOwnProperty(id)) {
  14806. var value = obj[id].getValue();
  14807. if (value !== undefined) {
  14808. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  14809. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  14810. }
  14811. }
  14812. }
  14813. // adjust the range of all objects
  14814. if (valueMin !== undefined && valueMax !== undefined) {
  14815. for (id in obj) {
  14816. if (obj.hasOwnProperty(id)) {
  14817. obj[id].setValueRange(valueMin, valueMax);
  14818. }
  14819. }
  14820. }
  14821. };
  14822. /**
  14823. * Redraw the graph with the current data
  14824. * chart will be resized too.
  14825. */
  14826. Graph.prototype.redraw = function() {
  14827. this.setSize(this.width, this.height);
  14828. this._redraw();
  14829. };
  14830. /**
  14831. * Redraw the graph with the current data
  14832. * @private
  14833. */
  14834. Graph.prototype._redraw = function() {
  14835. var ctx = this.frame.canvas.getContext('2d');
  14836. // clear the canvas
  14837. var w = this.frame.canvas.width;
  14838. var h = this.frame.canvas.height;
  14839. ctx.clearRect(0, 0, w, h);
  14840. // set scaling and translation
  14841. ctx.save();
  14842. ctx.translate(this.translation.x, this.translation.y);
  14843. ctx.scale(this.scale, this.scale);
  14844. this.canvasTopLeft = {
  14845. "x": this._canvasToX(0),
  14846. "y": this._canvasToY(0)
  14847. };
  14848. this.canvasBottomRight = {
  14849. "x": this._canvasToX(this.frame.canvas.clientWidth),
  14850. "y": this._canvasToY(this.frame.canvas.clientHeight)
  14851. };
  14852. this._doInAllSectors("_drawAllSectorNodes",ctx);
  14853. this._doInAllSectors("_drawEdges",ctx);
  14854. this._doInAllSectors("_drawNodes",ctx,false);
  14855. // this._doInSupportSector("_drawNodes",ctx,true);
  14856. // this._drawTree(ctx,"#F00F0F");
  14857. // restore original scaling and translation
  14858. ctx.restore();
  14859. };
  14860. /**
  14861. * Set the translation of the graph
  14862. * @param {Number} offsetX Horizontal offset
  14863. * @param {Number} offsetY Vertical offset
  14864. * @private
  14865. */
  14866. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  14867. if (this.translation === undefined) {
  14868. this.translation = {
  14869. x: 0,
  14870. y: 0
  14871. };
  14872. }
  14873. if (offsetX !== undefined) {
  14874. this.translation.x = offsetX;
  14875. }
  14876. if (offsetY !== undefined) {
  14877. this.translation.y = offsetY;
  14878. }
  14879. };
  14880. /**
  14881. * Get the translation of the graph
  14882. * @return {Object} translation An object with parameters x and y, both a number
  14883. * @private
  14884. */
  14885. Graph.prototype._getTranslation = function() {
  14886. return {
  14887. x: this.translation.x,
  14888. y: this.translation.y
  14889. };
  14890. };
  14891. /**
  14892. * Scale the graph
  14893. * @param {Number} scale Scaling factor 1.0 is unscaled
  14894. * @private
  14895. */
  14896. Graph.prototype._setScale = function(scale) {
  14897. this.scale = scale;
  14898. };
  14899. /**
  14900. * Get the current scale of the graph
  14901. * @return {Number} scale Scaling factor 1.0 is unscaled
  14902. * @private
  14903. */
  14904. Graph.prototype._getScale = function() {
  14905. return this.scale;
  14906. };
  14907. /**
  14908. * Convert a horizontal point on the HTML canvas to the x-value of the model
  14909. * @param {number} x
  14910. * @returns {number}
  14911. * @private
  14912. */
  14913. Graph.prototype._canvasToX = function(x) {
  14914. return (x - this.translation.x) / this.scale;
  14915. };
  14916. /**
  14917. * Convert an x-value in the model to a horizontal point on the HTML canvas
  14918. * @param {number} x
  14919. * @returns {number}
  14920. * @private
  14921. */
  14922. Graph.prototype._xToCanvas = function(x) {
  14923. return x * this.scale + this.translation.x;
  14924. };
  14925. /**
  14926. * Convert a vertical point on the HTML canvas to the y-value of the model
  14927. * @param {number} y
  14928. * @returns {number}
  14929. * @private
  14930. */
  14931. Graph.prototype._canvasToY = function(y) {
  14932. return (y - this.translation.y) / this.scale;
  14933. };
  14934. /**
  14935. * Convert an y-value in the model to a vertical point on the HTML canvas
  14936. * @param {number} y
  14937. * @returns {number}
  14938. * @private
  14939. */
  14940. Graph.prototype._yToCanvas = function(y) {
  14941. return y * this.scale + this.translation.y ;
  14942. };
  14943. /**
  14944. * Redraw all nodes
  14945. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  14946. * @param {CanvasRenderingContext2D} ctx
  14947. * @param {Boolean} [alwaysShow]
  14948. * @private
  14949. */
  14950. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  14951. if (alwaysShow === undefined) {
  14952. alwaysShow = false;
  14953. }
  14954. // first draw the unselected nodes
  14955. var nodes = this.nodes;
  14956. var selected = [];
  14957. for (var id in nodes) {
  14958. if (nodes.hasOwnProperty(id)) {
  14959. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  14960. if (nodes[id].isSelected()) {
  14961. selected.push(id);
  14962. }
  14963. else {
  14964. if (nodes[id].inArea() || alwaysShow) {
  14965. nodes[id].draw(ctx);
  14966. }
  14967. }
  14968. }
  14969. }
  14970. // draw the selected nodes on top
  14971. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  14972. if (nodes[selected[s]].inArea() || alwaysShow) {
  14973. nodes[selected[s]].draw(ctx);
  14974. }
  14975. }
  14976. };
  14977. /**
  14978. * Redraw all edges
  14979. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  14980. * @param {CanvasRenderingContext2D} ctx
  14981. * @private
  14982. */
  14983. Graph.prototype._drawEdges = function(ctx) {
  14984. var edges = this.edges;
  14985. for (var id in edges) {
  14986. if (edges.hasOwnProperty(id)) {
  14987. var edge = edges[id];
  14988. edge.setScale(this.scale);
  14989. if (edge.connected) {
  14990. edges[id].draw(ctx);
  14991. }
  14992. }
  14993. }
  14994. };
  14995. /**
  14996. * Find a stable position for all nodes
  14997. * @private
  14998. */
  14999. Graph.prototype._stabilize = function() {
  15000. if (this.constants.freezeForStabilization == true) {
  15001. this._freezeDefinedNodes();
  15002. }
  15003. // find stable position
  15004. var count = 0;
  15005. while (this.moving && count < this.constants.stabilizationIterations) {
  15006. this._physicsTick();
  15007. count++;
  15008. }
  15009. this.zoomExtent(false,true);
  15010. if (this.constants.freezeForStabilization == true) {
  15011. this._restoreFrozenNodes();
  15012. }
  15013. this.emit("stabilized",{iterations:count});
  15014. };
  15015. Graph.prototype._freezeDefinedNodes = function() {
  15016. var nodes = this.nodes;
  15017. for (var id in nodes) {
  15018. if (nodes.hasOwnProperty(id)) {
  15019. if (nodes[id].x != null && nodes[id].y != null) {
  15020. nodes[id].fixedData.x = nodes[id].xFixed;
  15021. nodes[id].fixedData.y = nodes[id].yFixed;
  15022. nodes[id].xFixed = true;
  15023. nodes[id].yFixed = true;
  15024. }
  15025. }
  15026. }
  15027. };
  15028. Graph.prototype._restoreFrozenNodes = function() {
  15029. var nodes = this.nodes;
  15030. for (var id in nodes) {
  15031. if (nodes.hasOwnProperty(id)) {
  15032. if (nodes[id].fixedData.x != null) {
  15033. nodes[id].xFixed = nodes[id].fixedData.x;
  15034. nodes[id].yFixed = nodes[id].fixedData.y;
  15035. }
  15036. }
  15037. }
  15038. };
  15039. /**
  15040. * Check if any of the nodes is still moving
  15041. * @param {number} vmin the minimum velocity considered as 'moving'
  15042. * @return {boolean} true if moving, false if non of the nodes is moving
  15043. * @private
  15044. */
  15045. Graph.prototype._isMoving = function(vmin) {
  15046. var nodes = this.nodes;
  15047. for (var id in nodes) {
  15048. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15049. return true;
  15050. }
  15051. }
  15052. return false;
  15053. };
  15054. /**
  15055. * /**
  15056. * Perform one discrete step for all nodes
  15057. *
  15058. * @private
  15059. */
  15060. Graph.prototype._discreteStepNodes = function() {
  15061. var interval = this.physicsDiscreteStepsize;
  15062. var nodes = this.nodes;
  15063. var nodeId;
  15064. var nodesPresent = false;
  15065. if (this.constants.maxVelocity > 0) {
  15066. for (nodeId in nodes) {
  15067. if (nodes.hasOwnProperty(nodeId)) {
  15068. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15069. nodesPresent = true;
  15070. }
  15071. }
  15072. }
  15073. else {
  15074. for (nodeId in nodes) {
  15075. if (nodes.hasOwnProperty(nodeId)) {
  15076. nodes[nodeId].discreteStep(interval);
  15077. nodesPresent = true;
  15078. }
  15079. }
  15080. }
  15081. if (nodesPresent == true) {
  15082. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15083. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15084. this.moving = true;
  15085. }
  15086. else {
  15087. this.moving = this._isMoving(vminCorrected);
  15088. }
  15089. }
  15090. };
  15091. Graph.prototype._physicsTick = function() {
  15092. if (!this.freezeSimulation) {
  15093. if (this.moving) {
  15094. this._doInAllActiveSectors("_initializeForceCalculation");
  15095. this._doInAllActiveSectors("_discreteStepNodes");
  15096. if (this.constants.smoothCurves) {
  15097. this._doInSupportSector("_discreteStepNodes");
  15098. }
  15099. this._findCenter(this._getRange())
  15100. }
  15101. }
  15102. };
  15103. /**
  15104. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15105. * It reschedules itself at the beginning of the function
  15106. *
  15107. * @private
  15108. */
  15109. Graph.prototype._animationStep = function() {
  15110. // reset the timer so a new scheduled animation step can be set
  15111. this.timer = undefined;
  15112. // handle the keyboad movement
  15113. this._handleNavigation();
  15114. // this schedules a new animation step
  15115. this.start();
  15116. // start the physics simulation
  15117. var calculationTime = Date.now();
  15118. var maxSteps = 1;
  15119. this._physicsTick();
  15120. var timeRequired = Date.now() - calculationTime;
  15121. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  15122. this._physicsTick();
  15123. timeRequired = Date.now() - calculationTime;
  15124. maxSteps++;
  15125. }
  15126. // start the rendering process
  15127. var renderTime = Date.now();
  15128. this._redraw();
  15129. this.renderTime = Date.now() - renderTime;
  15130. };
  15131. if (typeof window !== 'undefined') {
  15132. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  15133. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  15134. }
  15135. /**
  15136. * Schedule a animation step with the refreshrate interval.
  15137. */
  15138. Graph.prototype.start = function() {
  15139. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15140. if (!this.timer) {
  15141. var ua = navigator.userAgent.toLowerCase();
  15142. var requiresTimeout = false;
  15143. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  15144. requiresTimeout = true;
  15145. }
  15146. else if (ua.indexOf('safari') != -1) { // safari
  15147. if (ua.indexOf('chrome') <= -1) {
  15148. requiresTimeout = true;
  15149. }
  15150. }
  15151. if (requiresTimeout == true) {
  15152. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15153. }
  15154. else{
  15155. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15156. }
  15157. }
  15158. }
  15159. else {
  15160. this._redraw();
  15161. }
  15162. };
  15163. /**
  15164. * Move the graph according to the keyboard presses.
  15165. *
  15166. * @private
  15167. */
  15168. Graph.prototype._handleNavigation = function() {
  15169. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15170. var translation = this._getTranslation();
  15171. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15172. }
  15173. if (this.zoomIncrement != 0) {
  15174. var center = {
  15175. x: this.frame.canvas.clientWidth / 2,
  15176. y: this.frame.canvas.clientHeight / 2
  15177. };
  15178. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15179. }
  15180. };
  15181. /**
  15182. * Freeze the _animationStep
  15183. */
  15184. Graph.prototype.toggleFreeze = function() {
  15185. if (this.freezeSimulation == false) {
  15186. this.freezeSimulation = true;
  15187. }
  15188. else {
  15189. this.freezeSimulation = false;
  15190. this.start();
  15191. }
  15192. };
  15193. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15194. if (disableStart === undefined) {
  15195. disableStart = true;
  15196. }
  15197. if (this.constants.smoothCurves == true) {
  15198. this._createBezierNodes();
  15199. }
  15200. else {
  15201. // delete the support nodes
  15202. this.sectors['support']['nodes'] = {};
  15203. for (var edgeId in this.edges) {
  15204. if (this.edges.hasOwnProperty(edgeId)) {
  15205. this.edges[edgeId].smooth = false;
  15206. this.edges[edgeId].via = null;
  15207. }
  15208. }
  15209. }
  15210. this._updateCalculationNodes();
  15211. if (!disableStart) {
  15212. this.moving = true;
  15213. this.start();
  15214. }
  15215. };
  15216. Graph.prototype._createBezierNodes = function() {
  15217. if (this.constants.smoothCurves == true) {
  15218. for (var edgeId in this.edges) {
  15219. if (this.edges.hasOwnProperty(edgeId)) {
  15220. var edge = this.edges[edgeId];
  15221. if (edge.via == null) {
  15222. edge.smooth = true;
  15223. var nodeId = "edgeId:".concat(edge.id);
  15224. this.sectors['support']['nodes'][nodeId] = new Node(
  15225. {id:nodeId,
  15226. mass:1,
  15227. shape:'circle',
  15228. image:"",
  15229. internalMultiplier:1
  15230. },{},{},this.constants);
  15231. edge.via = this.sectors['support']['nodes'][nodeId];
  15232. edge.via.parentEdgeId = edge.id;
  15233. edge.positionBezierNode();
  15234. }
  15235. }
  15236. }
  15237. }
  15238. };
  15239. Graph.prototype._initializeMixinLoaders = function () {
  15240. for (var mixinFunction in graphMixinLoaders) {
  15241. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15242. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15243. }
  15244. }
  15245. };
  15246. /**
  15247. * Load the XY positions of the nodes into the dataset.
  15248. */
  15249. Graph.prototype.storePosition = function() {
  15250. var dataArray = [];
  15251. for (var nodeId in this.nodes) {
  15252. if (this.nodes.hasOwnProperty(nodeId)) {
  15253. var node = this.nodes[nodeId];
  15254. var allowedToMoveX = !this.nodes.xFixed;
  15255. var allowedToMoveY = !this.nodes.yFixed;
  15256. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  15257. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  15258. }
  15259. }
  15260. }
  15261. this.nodesData.update(dataArray);
  15262. };
  15263. /**
  15264. * vis.js module exports
  15265. */
  15266. var vis = {
  15267. util: util,
  15268. DataSet: DataSet,
  15269. DataView: DataView,
  15270. Range: Range,
  15271. Stack: Stack,
  15272. TimeStep: TimeStep,
  15273. components: {
  15274. items: {
  15275. Item: Item,
  15276. ItemBox: ItemBox,
  15277. ItemPoint: ItemPoint,
  15278. ItemRange: ItemRange
  15279. },
  15280. Component: Component,
  15281. Panel: Panel,
  15282. RootPanel: RootPanel,
  15283. ItemSet: ItemSet,
  15284. TimeAxis: TimeAxis
  15285. },
  15286. graph: {
  15287. Node: Node,
  15288. Edge: Edge,
  15289. Popup: Popup,
  15290. Groups: Groups,
  15291. Images: Images
  15292. },
  15293. Timeline: Timeline,
  15294. Graph: Graph
  15295. };
  15296. /**
  15297. * CommonJS module exports
  15298. */
  15299. if (typeof exports !== 'undefined') {
  15300. exports = vis;
  15301. }
  15302. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  15303. module.exports = vis;
  15304. }
  15305. /**
  15306. * AMD module exports
  15307. */
  15308. if (typeof(define) === 'function') {
  15309. define(function () {
  15310. return vis;
  15311. });
  15312. }
  15313. /**
  15314. * Window exports
  15315. */
  15316. if (typeof window !== 'undefined') {
  15317. // attach the module to the window, load as a regular javascript file
  15318. window['vis'] = vis;
  15319. }