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.

12625 lines
378 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.1.0-SNAPSHOT
  8. * @date 2013-06-17
  9. *
  10. * @license
  11. * Copyright (C) 2011-2013 Almende B.V, http://almende.com
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  14. * use this file except in compliance with the License. You may obtain a copy
  15. * of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  21. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  22. * License for the specific language governing permissions and limitations under
  23. * the License.
  24. */
  25. (function(e){if("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports;
  26. return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
  27. (function(){/**
  28. * vis.js module imports
  29. */
  30. var moment = require('moment');
  31. /**
  32. * utility functions
  33. */
  34. var util = {};
  35. /**
  36. * Test whether given object is a number
  37. * @param {*} object
  38. * @return {Boolean} isNumber
  39. */
  40. util.isNumber = function isNumber(object) {
  41. return (object instanceof Number || typeof object == 'number');
  42. };
  43. /**
  44. * Test whether given object is a string
  45. * @param {*} object
  46. * @return {Boolean} isString
  47. */
  48. util.isString = function isString(object) {
  49. return (object instanceof String || typeof object == 'string');
  50. };
  51. /**
  52. * Test whether given object is a Date, or a String containing a Date
  53. * @param {Date | String} object
  54. * @return {Boolean} isDate
  55. */
  56. util.isDate = function isDate(object) {
  57. if (object instanceof Date) {
  58. return true;
  59. }
  60. else if (util.isString(object)) {
  61. // test whether this string contains a date
  62. var match = ASPDateRegex.exec(object);
  63. if (match) {
  64. return true;
  65. }
  66. else if (!isNaN(Date.parse(object))) {
  67. return true;
  68. }
  69. }
  70. return false;
  71. };
  72. /**
  73. * Test whether given object is an instance of google.visualization.DataTable
  74. * @param {*} object
  75. * @return {Boolean} isDataTable
  76. */
  77. util.isDataTable = function isDataTable(object) {
  78. return (typeof (google) !== 'undefined') &&
  79. (google.visualization) &&
  80. (google.visualization.DataTable) &&
  81. (object instanceof google.visualization.DataTable);
  82. };
  83. /**
  84. * Create a semi UUID
  85. * source: http://stackoverflow.com/a/105074/1262753
  86. * @return {String} uuid
  87. */
  88. util.randomUUID = function randomUUID () {
  89. var S4 = function () {
  90. return Math.floor(
  91. Math.random() * 0x10000 /* 65536 */
  92. ).toString(16);
  93. };
  94. return (
  95. S4() + S4() + '-' +
  96. S4() + '-' +
  97. S4() + '-' +
  98. S4() + '-' +
  99. S4() + S4() + S4()
  100. );
  101. };
  102. /**
  103. * Extend object a with the properties of object b or a series of objects
  104. * Only properties with defined values are copied
  105. * @param {Object} a
  106. * @param {... Object} b
  107. * @return {Object} a
  108. */
  109. util.extend = function (a, b) {
  110. for (var i = 1, len = arguments.length; i < len; i++) {
  111. var other = arguments[i];
  112. for (var prop in other) {
  113. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  114. a[prop] = other[prop];
  115. }
  116. }
  117. }
  118. return a;
  119. };
  120. /**
  121. * Cast an object to another type
  122. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  123. * @param {String | undefined} type Name of the type. Available types:
  124. * 'Boolean', 'Number', 'String',
  125. * 'Date', 'Moment', ISODate', 'ASPDate'.
  126. * @return {*} object
  127. * @throws Error
  128. */
  129. util.cast = function cast(object, type) {
  130. var match;
  131. if (object === undefined) {
  132. return undefined;
  133. }
  134. if (object === null) {
  135. return null;
  136. }
  137. if (!type) {
  138. return object;
  139. }
  140. if (!(typeof type === 'string') && !(type instanceof String)) {
  141. throw new Error('Type must be a string');
  142. }
  143. //noinspection FallthroughInSwitchStatementJS
  144. switch (type) {
  145. case 'boolean':
  146. case 'Boolean':
  147. return Boolean(object);
  148. case 'number':
  149. case 'Number':
  150. return Number(object);
  151. case 'string':
  152. case 'String':
  153. return String(object);
  154. case 'Date':
  155. if (util.isNumber(object)) {
  156. return new Date(object);
  157. }
  158. if (object instanceof Date) {
  159. return new Date(object.valueOf());
  160. }
  161. else if (moment.isMoment(object)) {
  162. return new Date(object.valueOf());
  163. }
  164. if (util.isString(object)) {
  165. // parse ASP.Net Date pattern,
  166. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  167. // code from http://momentjs.com/
  168. match = ASPDateRegex.exec(object);
  169. if (match) {
  170. return new Date(Number(match[1])); // parse number
  171. }
  172. else {
  173. return moment(object).toDate(); // parse string
  174. }
  175. }
  176. else {
  177. throw new Error(
  178. 'Cannot cast object of type ' + util.getType(object) +
  179. ' to type Date');
  180. }
  181. case 'Moment':
  182. if (util.isNumber(object)) {
  183. return moment(object);
  184. }
  185. if (object instanceof Date) {
  186. return moment(object.valueOf());
  187. }
  188. else if (moment.isMoment(object)) {
  189. return moment.clone();
  190. }
  191. if (util.isString(object)) {
  192. // parse ASP.Net Date pattern,
  193. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  194. // code from http://momentjs.com/
  195. match = ASPDateRegex.exec(object);
  196. if (match) {
  197. return moment(Number(match[1])); // parse number
  198. }
  199. else {
  200. return moment(object); // parse string
  201. }
  202. }
  203. else {
  204. throw new Error(
  205. 'Cannot cast object of type ' + util.getType(object) +
  206. ' to type Date');
  207. }
  208. case 'ISODate':
  209. if (object instanceof Date) {
  210. return object.toISOString();
  211. }
  212. else if (moment.isMoment(object)) {
  213. return object.toDate().toISOString();
  214. }
  215. else if (util.isNumber(object) || util.isString(object)) {
  216. return moment(object).toDate().toISOString();
  217. }
  218. else {
  219. throw new Error(
  220. 'Cannot cast object of type ' + util.getType(object) +
  221. ' to type ISODate');
  222. }
  223. case 'ASPDate':
  224. if (object instanceof Date) {
  225. return '/Date(' + object.valueOf() + ')/';
  226. }
  227. else if (util.isNumber(object) || util.isString(object)) {
  228. return '/Date(' + moment(object).valueOf() + ')/';
  229. }
  230. else {
  231. throw new Error(
  232. 'Cannot cast object of type ' + util.getType(object) +
  233. ' to type ASPDate');
  234. }
  235. default:
  236. throw new Error('Cannot cast object of type ' + util.getType(object) +
  237. ' to type "' + type + '"');
  238. }
  239. };
  240. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  241. /**
  242. * Get the type of an object, for example util.getType([]) returns 'Array'
  243. * @param {*} object
  244. * @return {String} type
  245. */
  246. util.getType = function getType(object) {
  247. var type = typeof object;
  248. if (type == 'object') {
  249. if (object == null) {
  250. return 'null';
  251. }
  252. if (object instanceof Boolean) {
  253. return 'Boolean';
  254. }
  255. if (object instanceof Number) {
  256. return 'Number';
  257. }
  258. if (object instanceof String) {
  259. return 'String';
  260. }
  261. if (object instanceof Array) {
  262. return 'Array';
  263. }
  264. if (object instanceof Date) {
  265. return 'Date';
  266. }
  267. return 'Object';
  268. }
  269. else if (type == 'number') {
  270. return 'Number';
  271. }
  272. else if (type == 'boolean') {
  273. return 'Boolean';
  274. }
  275. else if (type == 'string') {
  276. return 'String';
  277. }
  278. return type;
  279. };
  280. /**
  281. * Retrieve the absolute left value of a DOM element
  282. * @param {Element} elem A dom element, for example a div
  283. * @return {number} left The absolute left position of this element
  284. * in the browser page.
  285. */
  286. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  287. var doc = document.documentElement;
  288. var body = document.body;
  289. var left = elem.offsetLeft;
  290. var e = elem.offsetParent;
  291. while (e != null && e != body && e != doc) {
  292. left += e.offsetLeft;
  293. left -= e.scrollLeft;
  294. e = e.offsetParent;
  295. }
  296. return left;
  297. };
  298. /**
  299. * Retrieve the absolute top value of a DOM element
  300. * @param {Element} elem A dom element, for example a div
  301. * @return {number} top The absolute top position of this element
  302. * in the browser page.
  303. */
  304. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  305. var doc = document.documentElement;
  306. var body = document.body;
  307. var top = elem.offsetTop;
  308. var e = elem.offsetParent;
  309. while (e != null && e != body && e != doc) {
  310. top += e.offsetTop;
  311. top -= e.scrollTop;
  312. e = e.offsetParent;
  313. }
  314. return top;
  315. };
  316. /**
  317. * Get the absolute, vertical mouse position from an event.
  318. * @param {Event} event
  319. * @return {Number} pageY
  320. */
  321. util.getPageY = function getPageY (event) {
  322. if ('pageY' in event) {
  323. return event.pageY;
  324. }
  325. else {
  326. var clientY;
  327. if (('targetTouches' in event) && event.targetTouches.length) {
  328. clientY = event.targetTouches[0].clientY;
  329. }
  330. else {
  331. clientY = event.clientY;
  332. }
  333. var doc = document.documentElement;
  334. var body = document.body;
  335. return clientY +
  336. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  337. ( doc && doc.clientTop || body && body.clientTop || 0 );
  338. }
  339. };
  340. /**
  341. * Get the absolute, horizontal mouse position from an event.
  342. * @param {Event} event
  343. * @return {Number} pageX
  344. */
  345. util.getPageX = function getPageX (event) {
  346. if ('pageY' in event) {
  347. return event.pageX;
  348. }
  349. else {
  350. var clientX;
  351. if (('targetTouches' in event) && event.targetTouches.length) {
  352. clientX = event.targetTouches[0].clientX;
  353. }
  354. else {
  355. clientX = event.clientX;
  356. }
  357. var doc = document.documentElement;
  358. var body = document.body;
  359. return clientX +
  360. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  361. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  362. }
  363. };
  364. /**
  365. * add a className to the given elements style
  366. * @param {Element} elem
  367. * @param {String} className
  368. */
  369. util.addClassName = function addClassName(elem, className) {
  370. var classes = elem.className.split(' ');
  371. if (classes.indexOf(className) == -1) {
  372. classes.push(className); // add the class to the array
  373. elem.className = classes.join(' ');
  374. }
  375. };
  376. /**
  377. * add a className to the given elements style
  378. * @param {Element} elem
  379. * @param {String} className
  380. */
  381. util.removeClassName = function removeClassname(elem, className) {
  382. var classes = elem.className.split(' ');
  383. var index = classes.indexOf(className);
  384. if (index != -1) {
  385. classes.splice(index, 1); // remove the class from the array
  386. elem.className = classes.join(' ');
  387. }
  388. };
  389. /**
  390. * For each method for both arrays and objects.
  391. * In case of an array, the built-in Array.forEach() is applied.
  392. * In case of an Object, the method loops over all properties of the object.
  393. * @param {Object | Array} object An Object or Array
  394. * @param {function} callback Callback method, called for each item in
  395. * the object or array with three parameters:
  396. * callback(value, index, object)
  397. */
  398. util.forEach = function forEach (object, callback) {
  399. var i,
  400. len;
  401. if (object instanceof Array) {
  402. // array
  403. for (i = 0, len = object.length; i < len; i++) {
  404. callback(object[i], i, object);
  405. }
  406. }
  407. else {
  408. // object
  409. for (i in object) {
  410. if (object.hasOwnProperty(i)) {
  411. callback(object[i], i, object);
  412. }
  413. }
  414. }
  415. };
  416. /**
  417. * Update a property in an object
  418. * @param {Object} object
  419. * @param {String} key
  420. * @param {*} value
  421. * @return {Boolean} changed
  422. */
  423. util.updateProperty = function updateProp (object, key, value) {
  424. if (object[key] !== value) {
  425. object[key] = value;
  426. return true;
  427. }
  428. else {
  429. return false;
  430. }
  431. };
  432. /**
  433. * Add and event listener. Works for all browsers
  434. * @param {Element} element An html element
  435. * @param {string} action The action, for example "click",
  436. * without the prefix "on"
  437. * @param {function} listener The callback function to be executed
  438. * @param {boolean} [useCapture]
  439. */
  440. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  441. if (element.addEventListener) {
  442. if (useCapture === undefined)
  443. useCapture = false;
  444. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  445. action = "DOMMouseScroll"; // For Firefox
  446. }
  447. element.addEventListener(action, listener, useCapture);
  448. } else {
  449. element.attachEvent("on" + action, listener); // IE browsers
  450. }
  451. };
  452. /**
  453. * Remove an event listener from an element
  454. * @param {Element} element An html dom element
  455. * @param {string} action The name of the event, for example "mousedown"
  456. * @param {function} listener The listener function
  457. * @param {boolean} [useCapture]
  458. */
  459. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  460. if (element.removeEventListener) {
  461. // non-IE browsers
  462. if (useCapture === undefined)
  463. useCapture = false;
  464. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  465. action = "DOMMouseScroll"; // For Firefox
  466. }
  467. element.removeEventListener(action, listener, useCapture);
  468. } else {
  469. // IE browsers
  470. element.detachEvent("on" + action, listener);
  471. }
  472. };
  473. /**
  474. * Get HTML element which is the target of the event
  475. * @param {Event} event
  476. * @return {Element} target element
  477. */
  478. util.getTarget = function getTarget(event) {
  479. // code from http://www.quirksmode.org/js/events_properties.html
  480. if (!event) {
  481. event = window.event;
  482. }
  483. var target;
  484. if (event.target) {
  485. target = event.target;
  486. }
  487. else if (event.srcElement) {
  488. target = event.srcElement;
  489. }
  490. if (target.nodeType != undefined && target.nodeType == 3) {
  491. // defeat Safari bug
  492. target = target.parentNode;
  493. }
  494. return target;
  495. };
  496. /**
  497. * Stop event propagation
  498. */
  499. util.stopPropagation = function stopPropagation(event) {
  500. if (!event)
  501. event = window.event;
  502. if (event.stopPropagation) {
  503. event.stopPropagation(); // non-IE browsers
  504. }
  505. else {
  506. event.cancelBubble = true; // IE browsers
  507. }
  508. };
  509. /**
  510. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  511. */
  512. util.preventDefault = function preventDefault (event) {
  513. if (!event)
  514. event = window.event;
  515. if (event.preventDefault) {
  516. event.preventDefault(); // non-IE browsers
  517. }
  518. else {
  519. event.returnValue = false; // IE browsers
  520. }
  521. };
  522. util.option = {};
  523. /**
  524. * Cast a value as boolean
  525. * @param {Boolean | function | undefined} value
  526. * @param {Boolean} [defaultValue]
  527. * @returns {Boolean} bool
  528. */
  529. util.option.asBoolean = function (value, defaultValue) {
  530. if (typeof value == 'function') {
  531. value = value();
  532. }
  533. if (value != null) {
  534. return (value != false);
  535. }
  536. return defaultValue || null;
  537. };
  538. /**
  539. * Cast a value as number
  540. * @param {Boolean | function | undefined} value
  541. * @param {Number} [defaultValue]
  542. * @returns {Number} number
  543. */
  544. util.option.asNumber = function (value, defaultValue) {
  545. if (typeof value == 'function') {
  546. value = value();
  547. }
  548. if (value != null) {
  549. return Number(value) || defaultValue || null;
  550. }
  551. return defaultValue || null;
  552. };
  553. /**
  554. * Cast a value as string
  555. * @param {String | function | undefined} value
  556. * @param {String} [defaultValue]
  557. * @returns {String} str
  558. */
  559. util.option.asString = function (value, defaultValue) {
  560. if (typeof value == 'function') {
  561. value = value();
  562. }
  563. if (value != null) {
  564. return String(value);
  565. }
  566. return defaultValue || null;
  567. };
  568. /**
  569. * Cast a size or location in pixels or a percentage
  570. * @param {String | Number | function | undefined} value
  571. * @param {String} [defaultValue]
  572. * @returns {String} size
  573. */
  574. util.option.asSize = function (value, defaultValue) {
  575. if (typeof value == 'function') {
  576. value = value();
  577. }
  578. if (util.isString(value)) {
  579. return value;
  580. }
  581. else if (util.isNumber(value)) {
  582. return value + 'px';
  583. }
  584. else {
  585. return defaultValue || null;
  586. }
  587. };
  588. /**
  589. * Cast a value as DOM element
  590. * @param {HTMLElement | function | undefined} value
  591. * @param {HTMLElement} [defaultValue]
  592. * @returns {HTMLElement | null} dom
  593. */
  594. util.option.asElement = function (value, defaultValue) {
  595. if (typeof value == 'function') {
  596. value = value();
  597. }
  598. return value || defaultValue || null;
  599. };
  600. /**
  601. * load css from text
  602. * @param {String} css Text containing css
  603. */
  604. util.loadCss = function (css) {
  605. if (typeof document === 'undefined') {
  606. return;
  607. }
  608. // get the script location, and built the css file name from the js file name
  609. // http://stackoverflow.com/a/2161748/1262753
  610. // var scripts = document.getElementsByTagName('script');
  611. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  612. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  613. // inject css
  614. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  615. var style = document.createElement('style');
  616. style.type = 'text/css';
  617. if (style.styleSheet){
  618. style.styleSheet.cssText = css;
  619. } else {
  620. style.appendChild(document.createTextNode(css));
  621. }
  622. document.getElementsByTagName('head')[0].appendChild(style);
  623. };
  624. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  625. // it here in that case.
  626. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  627. if(!Array.prototype.indexOf) {
  628. Array.prototype.indexOf = function(obj){
  629. for(var i = 0; i < this.length; i++){
  630. if(this[i] == obj){
  631. return i;
  632. }
  633. }
  634. return -1;
  635. };
  636. try {
  637. console.log("Warning: Ancient browser detected. Please update your browser");
  638. }
  639. catch (err) {
  640. }
  641. }
  642. // Internet Explorer 8 and older does not support Array.forEach, so we define
  643. // it here in that case.
  644. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  645. if (!Array.prototype.forEach) {
  646. Array.prototype.forEach = function(fn, scope) {
  647. for(var i = 0, len = this.length; i < len; ++i) {
  648. fn.call(scope || this, this[i], i, this);
  649. }
  650. }
  651. }
  652. // Internet Explorer 8 and older does not support Array.map, so we define it
  653. // here in that case.
  654. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  655. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  656. // Reference: http://es5.github.com/#x15.4.4.19
  657. if (!Array.prototype.map) {
  658. Array.prototype.map = function(callback, thisArg) {
  659. var T, A, k;
  660. if (this == null) {
  661. throw new TypeError(" this is null or not defined");
  662. }
  663. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  664. var O = Object(this);
  665. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  666. // 3. Let len be ToUint32(lenValue).
  667. var len = O.length >>> 0;
  668. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  669. // See: http://es5.github.com/#x9.11
  670. if (typeof callback !== "function") {
  671. throw new TypeError(callback + " is not a function");
  672. }
  673. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  674. if (thisArg) {
  675. T = thisArg;
  676. }
  677. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  678. // the standard built-in constructor with that name and len is the value of len.
  679. A = new Array(len);
  680. // 7. Let k be 0
  681. k = 0;
  682. // 8. Repeat, while k < len
  683. while(k < len) {
  684. var kValue, mappedValue;
  685. // a. Let Pk be ToString(k).
  686. // This is implicit for LHS operands of the in operator
  687. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  688. // This step can be combined with c
  689. // c. If kPresent is true, then
  690. if (k in O) {
  691. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  692. kValue = O[ k ];
  693. // ii. Let mappedValue be the result of calling the Call internal method of callback
  694. // with T as the this value and argument list containing kValue, k, and O.
  695. mappedValue = callback.call(T, kValue, k, O);
  696. // iii. Call the DefineOwnProperty internal method of A with arguments
  697. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  698. // and false.
  699. // In browsers that support Object.defineProperty, use the following:
  700. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  701. // For best browser support, use the following:
  702. A[ k ] = mappedValue;
  703. }
  704. // d. Increase k by 1.
  705. k++;
  706. }
  707. // 9. return A
  708. return A;
  709. };
  710. }
  711. // Internet Explorer 8 and older does not support Array.filter, so we define it
  712. // here in that case.
  713. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  714. if (!Array.prototype.filter) {
  715. Array.prototype.filter = function(fun /*, thisp */) {
  716. "use strict";
  717. if (this == null) {
  718. throw new TypeError();
  719. }
  720. var t = Object(this);
  721. var len = t.length >>> 0;
  722. if (typeof fun != "function") {
  723. throw new TypeError();
  724. }
  725. var res = [];
  726. var thisp = arguments[1];
  727. for (var i = 0; i < len; i++) {
  728. if (i in t) {
  729. var val = t[i]; // in case fun mutates this
  730. if (fun.call(thisp, val, i, t))
  731. res.push(val);
  732. }
  733. }
  734. return res;
  735. };
  736. }
  737. // Internet Explorer 8 and older does not support Object.keys, so we define it
  738. // here in that case.
  739. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  740. if (!Object.keys) {
  741. Object.keys = (function () {
  742. var hasOwnProperty = Object.prototype.hasOwnProperty,
  743. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  744. dontEnums = [
  745. 'toString',
  746. 'toLocaleString',
  747. 'valueOf',
  748. 'hasOwnProperty',
  749. 'isPrototypeOf',
  750. 'propertyIsEnumerable',
  751. 'constructor'
  752. ],
  753. dontEnumsLength = dontEnums.length;
  754. return function (obj) {
  755. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  756. throw new TypeError('Object.keys called on non-object');
  757. }
  758. var result = [];
  759. for (var prop in obj) {
  760. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  761. }
  762. if (hasDontEnumBug) {
  763. for (var i=0; i < dontEnumsLength; i++) {
  764. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  765. }
  766. }
  767. return result;
  768. }
  769. })()
  770. }
  771. // Internet Explorer 8 and older does not support Array.isArray,
  772. // so we define it here in that case.
  773. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  774. if(!Array.isArray) {
  775. Array.isArray = function (vArg) {
  776. return Object.prototype.toString.call(vArg) === "[object Array]";
  777. };
  778. }
  779. // Internet Explorer 8 and older does not support Function.bind,
  780. // so we define it here in that case.
  781. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  782. if (!Function.prototype.bind) {
  783. Function.prototype.bind = function (oThis) {
  784. if (typeof this !== "function") {
  785. // closest thing possible to the ECMAScript 5 internal IsCallable function
  786. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  787. }
  788. var aArgs = Array.prototype.slice.call(arguments, 1),
  789. fToBind = this,
  790. fNOP = function () {},
  791. fBound = function () {
  792. return fToBind.apply(this instanceof fNOP && oThis
  793. ? this
  794. : oThis,
  795. aArgs.concat(Array.prototype.slice.call(arguments)));
  796. };
  797. fNOP.prototype = this.prototype;
  798. fBound.prototype = new fNOP();
  799. return fBound;
  800. };
  801. }
  802. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  803. if (!Object.create) {
  804. Object.create = function (o) {
  805. if (arguments.length > 1) {
  806. throw new Error('Object.create implementation only accepts the first parameter.');
  807. }
  808. function F() {}
  809. F.prototype = o;
  810. return new F();
  811. };
  812. }
  813. /**
  814. * Event listener (singleton)
  815. */
  816. // TODO: replace usage of the event listener for the EventBus
  817. var events = {
  818. 'listeners': [],
  819. /**
  820. * Find a single listener by its object
  821. * @param {Object} object
  822. * @return {Number} index -1 when not found
  823. */
  824. 'indexOf': function (object) {
  825. var listeners = this.listeners;
  826. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  827. var listener = listeners[i];
  828. if (listener && listener.object == object) {
  829. return i;
  830. }
  831. }
  832. return -1;
  833. },
  834. /**
  835. * Add an event listener
  836. * @param {Object} object
  837. * @param {String} event The name of an event, for example 'select'
  838. * @param {function} callback The callback method, called when the
  839. * event takes place
  840. */
  841. 'addListener': function (object, event, callback) {
  842. var index = this.indexOf(object);
  843. var listener = this.listeners[index];
  844. if (!listener) {
  845. listener = {
  846. 'object': object,
  847. 'events': {}
  848. };
  849. this.listeners.push(listener);
  850. }
  851. var callbacks = listener.events[event];
  852. if (!callbacks) {
  853. callbacks = [];
  854. listener.events[event] = callbacks;
  855. }
  856. // add the callback if it does not yet exist
  857. if (callbacks.indexOf(callback) == -1) {
  858. callbacks.push(callback);
  859. }
  860. },
  861. /**
  862. * Remove an event listener
  863. * @param {Object} object
  864. * @param {String} event The name of an event, for example 'select'
  865. * @param {function} callback The registered callback method
  866. */
  867. 'removeListener': function (object, event, callback) {
  868. var index = this.indexOf(object);
  869. var listener = this.listeners[index];
  870. if (listener) {
  871. var callbacks = listener.events[event];
  872. if (callbacks) {
  873. index = callbacks.indexOf(callback);
  874. if (index != -1) {
  875. callbacks.splice(index, 1);
  876. }
  877. // remove the array when empty
  878. if (callbacks.length == 0) {
  879. delete listener.events[event];
  880. }
  881. }
  882. // count the number of registered events. remove listener when empty
  883. var count = 0;
  884. var events = listener.events;
  885. for (var e in events) {
  886. if (events.hasOwnProperty(e)) {
  887. count++;
  888. }
  889. }
  890. if (count == 0) {
  891. delete this.listeners[index];
  892. }
  893. }
  894. },
  895. /**
  896. * Remove all registered event listeners
  897. */
  898. 'removeAllListeners': function () {
  899. this.listeners = [];
  900. },
  901. /**
  902. * Trigger an event. All registered event handlers will be called
  903. * @param {Object} object
  904. * @param {String} event
  905. * @param {Object} properties (optional)
  906. */
  907. 'trigger': function (object, event, properties) {
  908. var index = this.indexOf(object);
  909. var listener = this.listeners[index];
  910. if (listener) {
  911. var callbacks = listener.events[event];
  912. if (callbacks) {
  913. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  914. callbacks[i](properties);
  915. }
  916. }
  917. }
  918. }
  919. };
  920. /**
  921. * An event bus can be used to emit events, and to subscribe to events
  922. * @constructor EventBus
  923. */
  924. function EventBus() {
  925. this.subscriptions = [];
  926. }
  927. /**
  928. * Subscribe to an event
  929. * @param {String | RegExp} event The event can be a regular expression, or
  930. * a string with wildcards, like 'server.*'.
  931. * @param {function} callback. Callback are called with three parameters:
  932. * {String} event, {*} [data], {*} [source]
  933. * @param {*} [target]
  934. * @returns {String} id A subscription id
  935. */
  936. EventBus.prototype.on = function (event, callback, target) {
  937. var regexp = (event instanceof RegExp) ?
  938. event :
  939. new RegExp(event.replace('*', '\\w+'));
  940. var subscription = {
  941. id: util.randomUUID(),
  942. event: event,
  943. regexp: regexp,
  944. callback: (typeof callback === 'function') ? callback : null,
  945. target: target
  946. };
  947. this.subscriptions.push(subscription);
  948. return subscription.id;
  949. };
  950. /**
  951. * Unsubscribe from an event
  952. * @param {String | Object} filter Filter for subscriptions to be removed
  953. * Filter can be a string containing a
  954. * subscription id, or an object containing
  955. * one or more of the fields id, event,
  956. * callback, and target.
  957. */
  958. EventBus.prototype.off = function (filter) {
  959. var i = 0;
  960. while (i < this.subscriptions.length) {
  961. var subscription = this.subscriptions[i];
  962. var match = true;
  963. if (filter instanceof Object) {
  964. // filter is an object. All fields must match
  965. for (var prop in filter) {
  966. if (filter.hasOwnProperty(prop)) {
  967. if (filter[prop] !== subscription[prop]) {
  968. match = false;
  969. }
  970. }
  971. }
  972. }
  973. else {
  974. // filter is a string, filter on id
  975. match = (subscription.id == filter);
  976. }
  977. if (match) {
  978. this.subscriptions.splice(i, 1);
  979. }
  980. else {
  981. i++;
  982. }
  983. }
  984. };
  985. /**
  986. * Emit an event
  987. * @param {String} event
  988. * @param {*} [data]
  989. * @param {*} [source]
  990. */
  991. EventBus.prototype.emit = function (event, data, source) {
  992. for (var i =0; i < this.subscriptions.length; i++) {
  993. var subscription = this.subscriptions[i];
  994. if (subscription.regexp.test(event)) {
  995. if (subscription.callback) {
  996. subscription.callback(event, data, source);
  997. }
  998. }
  999. }
  1000. };
  1001. /**
  1002. * DataSet
  1003. *
  1004. * Usage:
  1005. * var dataSet = new DataSet({
  1006. * fieldId: '_id',
  1007. * fieldTypes: {
  1008. * // ...
  1009. * }
  1010. * });
  1011. *
  1012. * dataSet.add(item);
  1013. * dataSet.add(data);
  1014. * dataSet.update(item);
  1015. * dataSet.update(data);
  1016. * dataSet.remove(id);
  1017. * dataSet.remove(ids);
  1018. * var data = dataSet.get();
  1019. * var data = dataSet.get(id);
  1020. * var data = dataSet.get(ids);
  1021. * var data = dataSet.get(ids, options, data);
  1022. * dataSet.clear();
  1023. *
  1024. * A data set can:
  1025. * - add/remove/update data
  1026. * - gives triggers upon changes in the data
  1027. * - can import/export data in various data formats
  1028. *
  1029. * @param {Object} [options] Available options:
  1030. * {String} fieldId Field name of the id in the
  1031. * items, 'id' by default.
  1032. * {Object.<String, String} fieldTypes
  1033. * A map with field names as key,
  1034. * and the field type as value.
  1035. * @constructor DataSet
  1036. */
  1037. // TODO: add a DataSet constructor DataSet(data, options)
  1038. function DataSet (options) {
  1039. this.id = util.randomUUID();
  1040. this.options = options || {};
  1041. this.data = {}; // map with data indexed by id
  1042. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1043. this.fieldTypes = {}; // field types by field name
  1044. if (this.options.fieldTypes) {
  1045. for (var field in this.options.fieldTypes) {
  1046. if (this.options.fieldTypes.hasOwnProperty(field)) {
  1047. var value = this.options.fieldTypes[field];
  1048. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1049. this.fieldTypes[field] = 'Date';
  1050. }
  1051. else {
  1052. this.fieldTypes[field] = value;
  1053. }
  1054. }
  1055. }
  1056. }
  1057. // event subscribers
  1058. this.subscribers = {};
  1059. this.internalIds = {}; // internally generated id's
  1060. }
  1061. /**
  1062. * Subscribe to an event, add an event listener
  1063. * @param {String} event Event name. Available events: 'put', 'update',
  1064. * 'remove'
  1065. * @param {function} callback Callback method. Called with three parameters:
  1066. * {String} event
  1067. * {Object | null} params
  1068. * {String} senderId
  1069. * @param {String} [id] Optional id for the sender, used to filter
  1070. * events triggered by the sender itself.
  1071. */
  1072. DataSet.prototype.subscribe = function (event, callback, id) {
  1073. var subscribers = this.subscribers[event];
  1074. if (!subscribers) {
  1075. subscribers = [];
  1076. this.subscribers[event] = subscribers;
  1077. }
  1078. subscribers.push({
  1079. id: id ? String(id) : null,
  1080. callback: callback
  1081. });
  1082. };
  1083. /**
  1084. * Unsubscribe from an event, remove an event listener
  1085. * @param {String} event
  1086. * @param {function} callback
  1087. */
  1088. DataSet.prototype.unsubscribe = function (event, callback) {
  1089. var subscribers = this.subscribers[event];
  1090. if (subscribers) {
  1091. this.subscribers[event] = subscribers.filter(function (listener) {
  1092. return (listener.callback != callback);
  1093. });
  1094. }
  1095. };
  1096. /**
  1097. * Trigger an event
  1098. * @param {String} event
  1099. * @param {Object | null} params
  1100. * @param {String} [senderId] Optional id of the sender.
  1101. * @private
  1102. */
  1103. DataSet.prototype._trigger = function (event, params, senderId) {
  1104. if (event == '*') {
  1105. throw new Error('Cannot trigger event *');
  1106. }
  1107. var subscribers = [];
  1108. if (event in this.subscribers) {
  1109. subscribers = subscribers.concat(this.subscribers[event]);
  1110. }
  1111. if ('*' in this.subscribers) {
  1112. subscribers = subscribers.concat(this.subscribers['*']);
  1113. }
  1114. for (var i = 0; i < subscribers.length; i++) {
  1115. var subscriber = subscribers[i];
  1116. if (subscriber.callback) {
  1117. subscriber.callback(event, params, senderId || null);
  1118. }
  1119. }
  1120. };
  1121. /**
  1122. * Add data.
  1123. * Adding an item will fail when there already is an item with the same id.
  1124. * @param {Object | Array | DataTable} data
  1125. * @param {String} [senderId] Optional sender id
  1126. */
  1127. DataSet.prototype.add = function (data, senderId) {
  1128. var addedItems = [],
  1129. id,
  1130. me = this;
  1131. if (data instanceof Array) {
  1132. // Array
  1133. for (var i = 0, len = data.length; i < len; i++) {
  1134. id = me._addItem(data[i]);
  1135. addedItems.push(id);
  1136. }
  1137. }
  1138. else if (util.isDataTable(data)) {
  1139. // Google DataTable
  1140. var columns = this._getColumnNames(data);
  1141. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1142. var item = {};
  1143. for (var col = 0, cols = columns.length; col < cols; col++) {
  1144. var field = columns[col];
  1145. item[field] = data.getValue(row, col);
  1146. }
  1147. id = me._addItem(item);
  1148. addedItems.push(id);
  1149. }
  1150. }
  1151. else if (data instanceof Object) {
  1152. // Single item
  1153. id = me._addItem(data);
  1154. addedItems.push(id);
  1155. }
  1156. else {
  1157. throw new Error('Unknown dataType');
  1158. }
  1159. if (addedItems.length) {
  1160. this._trigger('add', {items: addedItems}, senderId);
  1161. }
  1162. };
  1163. /**
  1164. * Update existing items. When an item does not exist, it will be created
  1165. * @param {Object | Array | DataTable} data
  1166. * @param {String} [senderId] Optional sender id
  1167. */
  1168. DataSet.prototype.update = function (data, senderId) {
  1169. var addedItems = [],
  1170. updatedItems = [],
  1171. me = this,
  1172. fieldId = me.fieldId;
  1173. var addOrUpdate = function (item) {
  1174. var id = item[fieldId];
  1175. if (me.data[id]) {
  1176. // update item
  1177. id = me._updateItem(item);
  1178. updatedItems.push(id);
  1179. }
  1180. else {
  1181. // add new item
  1182. id = me._addItem(item);
  1183. addedItems.push(id);
  1184. }
  1185. };
  1186. if (data instanceof Array) {
  1187. // Array
  1188. for (var i = 0, len = data.length; i < len; i++) {
  1189. addOrUpdate(data[i]);
  1190. }
  1191. }
  1192. else if (util.isDataTable(data)) {
  1193. // Google DataTable
  1194. var columns = this._getColumnNames(data);
  1195. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1196. var item = {};
  1197. for (var col = 0, cols = columns.length; col < cols; col++) {
  1198. var field = columns[col];
  1199. item[field] = data.getValue(row, col);
  1200. }
  1201. addOrUpdate(item);
  1202. }
  1203. }
  1204. else if (data instanceof Object) {
  1205. // Single item
  1206. addOrUpdate(data);
  1207. }
  1208. else {
  1209. throw new Error('Unknown dataType');
  1210. }
  1211. if (addedItems.length) {
  1212. this._trigger('add', {items: addedItems}, senderId);
  1213. }
  1214. if (updatedItems.length) {
  1215. this._trigger('update', {items: updatedItems}, senderId);
  1216. }
  1217. };
  1218. /**
  1219. * Get a data item or multiple items.
  1220. *
  1221. * Usage:
  1222. *
  1223. * get()
  1224. * get(options: Object)
  1225. * get(options: Object, data: Array | DataTable)
  1226. *
  1227. * get(id: Number | String)
  1228. * get(id: Number | String, options: Object)
  1229. * get(id: Number | String, options: Object, data: Array | DataTable)
  1230. *
  1231. * get(ids: Number[] | String[])
  1232. * get(ids: Number[] | String[], options: Object)
  1233. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1234. *
  1235. * Where:
  1236. *
  1237. * {Number | String} id The id of an item
  1238. * {Number[] | String{}} ids An array with ids of items
  1239. * {Object} options An Object with options. Available options:
  1240. * {String} [type] Type of data to be returned. Can
  1241. * be 'DataTable' or 'Array' (default)
  1242. * {Object.<String, String>} [fieldTypes]
  1243. * {String[]} [fields] field names to be returned
  1244. * {function} [filter] filter items
  1245. * {String | function} [order] Order the items by
  1246. * a field name or custom sort function.
  1247. * {Array | DataTable} [data] If provided, items will be appended to this
  1248. * array or table. Required in case of Google
  1249. * DataTable.
  1250. *
  1251. * @throws Error
  1252. */
  1253. DataSet.prototype.get = function (args) {
  1254. var me = this;
  1255. // parse the arguments
  1256. var id, ids, options, data;
  1257. var firstType = util.getType(arguments[0]);
  1258. if (firstType == 'String' || firstType == 'Number') {
  1259. // get(id [, options] [, data])
  1260. id = arguments[0];
  1261. options = arguments[1];
  1262. data = arguments[2];
  1263. }
  1264. else if (firstType == 'Array') {
  1265. // get(ids [, options] [, data])
  1266. ids = arguments[0];
  1267. options = arguments[1];
  1268. data = arguments[2];
  1269. }
  1270. else {
  1271. // get([, options] [, data])
  1272. options = arguments[0];
  1273. data = arguments[1];
  1274. }
  1275. // determine the return type
  1276. var type;
  1277. if (options && options.type) {
  1278. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1279. if (data && (type != util.getType(data))) {
  1280. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1281. 'does not correspond with specified options.type (' + options.type + ')');
  1282. }
  1283. if (type == 'DataTable' && !util.isDataTable(data)) {
  1284. throw new Error('Parameter "data" must be a DataTable ' +
  1285. 'when options.type is "DataTable"');
  1286. }
  1287. }
  1288. else if (data) {
  1289. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1290. }
  1291. else {
  1292. type = 'Array';
  1293. }
  1294. // build options
  1295. var fieldTypes = options && options.fieldTypes || this.options.fieldTypes;
  1296. var filter = options && options.filter;
  1297. var items = [], item, itemId, i, len;
  1298. // cast items
  1299. if (id != undefined) {
  1300. // return a single item
  1301. item = me._getItem(id, fieldTypes);
  1302. if (filter && !filter(item)) {
  1303. item = null;
  1304. }
  1305. }
  1306. else if (ids != undefined) {
  1307. // return a subset of items
  1308. for (i = 0, len = ids.length; i < len; i++) {
  1309. item = me._getItem(ids[i], fieldTypes);
  1310. if (!filter || filter(item)) {
  1311. items.push(item);
  1312. }
  1313. }
  1314. }
  1315. else {
  1316. // return all items
  1317. for (itemId in this.data) {
  1318. if (this.data.hasOwnProperty(itemId)) {
  1319. item = me._getItem(itemId, fieldTypes);
  1320. if (!filter || filter(item)) {
  1321. items.push(item);
  1322. }
  1323. }
  1324. }
  1325. }
  1326. // order the results
  1327. if (options && options.order && id == undefined) {
  1328. this._sort(items, options.order);
  1329. }
  1330. // filter fields of the items
  1331. if (options && options.fields) {
  1332. var fields = options.fields;
  1333. if (id != undefined) {
  1334. item = this._filterFields(item, fields);
  1335. }
  1336. else {
  1337. for (i = 0, len = items.length; i < len; i++) {
  1338. items[i] = this._filterFields(items[i], fields);
  1339. }
  1340. }
  1341. }
  1342. // return the results
  1343. if (type == 'DataTable') {
  1344. var columns = this._getColumnNames(data);
  1345. if (id != undefined) {
  1346. // append a single item to the data table
  1347. me._appendRow(data, columns, item);
  1348. }
  1349. else {
  1350. // copy the items to the provided data table
  1351. for (i = 0, len = items.length; i < len; i++) {
  1352. me._appendRow(data, columns, items[i]);
  1353. }
  1354. }
  1355. return data;
  1356. }
  1357. else {
  1358. // return an array
  1359. if (id != undefined) {
  1360. // a single item
  1361. return item;
  1362. }
  1363. else {
  1364. // multiple items
  1365. if (data) {
  1366. // copy the items to the provided array
  1367. for (i = 0, len = items.length; i < len; i++) {
  1368. data.push(items[i]);
  1369. }
  1370. return data;
  1371. }
  1372. else {
  1373. // just return our array
  1374. return items;
  1375. }
  1376. }
  1377. }
  1378. };
  1379. /**
  1380. * Get ids of all items or from a filtered set of items.
  1381. * @param {Object} [options] An Object with options. Available options:
  1382. * {function} [filter] filter items
  1383. * {String | function} [order] Order the items by
  1384. * a field name or custom sort function.
  1385. * @return {Array} ids
  1386. */
  1387. DataSet.prototype.getIds = function (options) {
  1388. var data = this.data,
  1389. filter = options && options.filter,
  1390. order = options && options.order,
  1391. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1392. i,
  1393. len,
  1394. id,
  1395. item,
  1396. items,
  1397. ids = [];
  1398. if (filter) {
  1399. // get filtered items
  1400. if (order) {
  1401. // create ordered list
  1402. items = [];
  1403. for (id in data) {
  1404. if (data.hasOwnProperty(id)) {
  1405. item = this._getItem(id, fieldTypes);
  1406. if (filter(item)) {
  1407. items.push(item);
  1408. }
  1409. }
  1410. }
  1411. this._sort(items, order);
  1412. for (i = 0, len = items.length; i < len; i++) {
  1413. ids[i] = items[i][this.fieldId];
  1414. }
  1415. }
  1416. else {
  1417. // create unordered list
  1418. for (id in data) {
  1419. if (data.hasOwnProperty(id)) {
  1420. item = this._getItem(id, fieldTypes);
  1421. if (filter(item)) {
  1422. ids.push(item[this.fieldId]);
  1423. }
  1424. }
  1425. }
  1426. }
  1427. }
  1428. else {
  1429. // get all items
  1430. if (order) {
  1431. // create an ordered list
  1432. items = [];
  1433. for (id in data) {
  1434. if (data.hasOwnProperty(id)) {
  1435. items.push(data[id]);
  1436. }
  1437. }
  1438. this._sort(items, order);
  1439. for (i = 0, len = items.length; i < len; i++) {
  1440. ids[i] = items[i][this.fieldId];
  1441. }
  1442. }
  1443. else {
  1444. // create unordered list
  1445. for (id in data) {
  1446. if (data.hasOwnProperty(id)) {
  1447. item = data[id];
  1448. ids.push(item[this.fieldId]);
  1449. }
  1450. }
  1451. }
  1452. }
  1453. return ids;
  1454. };
  1455. /**
  1456. * Execute a callback function for every item in the dataset.
  1457. * The order of the items is not determined.
  1458. * @param {function} callback
  1459. * @param {Object} [options] Available options:
  1460. * {Object.<String, String>} [fieldTypes]
  1461. * {String[]} [fields] filter fields
  1462. * {function} [filter] filter items
  1463. * {String | function} [order] Order the items by
  1464. * a field name or custom sort function.
  1465. */
  1466. DataSet.prototype.forEach = function (callback, options) {
  1467. var filter = options && options.filter,
  1468. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1469. data = this.data,
  1470. item,
  1471. id;
  1472. if (options && options.order) {
  1473. // execute forEach on ordered list
  1474. var items = this.get(options);
  1475. for (var i = 0, len = items.length; i < len; i++) {
  1476. item = items[i];
  1477. id = item[this.fieldId];
  1478. callback(item, id);
  1479. }
  1480. }
  1481. else {
  1482. // unordered
  1483. for (id in data) {
  1484. if (data.hasOwnProperty(id)) {
  1485. item = this._getItem(id, fieldTypes);
  1486. if (!filter || filter(item)) {
  1487. callback(item, id);
  1488. }
  1489. }
  1490. }
  1491. }
  1492. };
  1493. /**
  1494. * Map every item in the dataset.
  1495. * @param {function} callback
  1496. * @param {Object} [options] Available options:
  1497. * {Object.<String, String>} [fieldTypes]
  1498. * {String[]} [fields] filter fields
  1499. * {function} [filter] filter items
  1500. * {String | function} [order] Order the items by
  1501. * a field name or custom sort function.
  1502. * @return {Object[]} mappedItems
  1503. */
  1504. DataSet.prototype.map = function (callback, options) {
  1505. var filter = options && options.filter,
  1506. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1507. mappedItems = [],
  1508. data = this.data,
  1509. item;
  1510. // cast and filter items
  1511. for (var id in data) {
  1512. if (data.hasOwnProperty(id)) {
  1513. item = this._getItem(id, fieldTypes);
  1514. if (!filter || filter(item)) {
  1515. mappedItems.push(callback(item, id));
  1516. }
  1517. }
  1518. }
  1519. // order items
  1520. if (options && options.order) {
  1521. this._sort(mappedItems, options.order);
  1522. }
  1523. return mappedItems;
  1524. };
  1525. /**
  1526. * Filter the fields of an item
  1527. * @param {Object} item
  1528. * @param {String[]} fields Field names
  1529. * @return {Object} filteredItem
  1530. * @private
  1531. */
  1532. DataSet.prototype._filterFields = function (item, fields) {
  1533. var filteredItem = {};
  1534. for (var field in item) {
  1535. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1536. filteredItem[field] = item[field];
  1537. }
  1538. }
  1539. return filteredItem;
  1540. };
  1541. /**
  1542. * Sort the provided array with items
  1543. * @param {Object[]} items
  1544. * @param {String | function} order A field name or custom sort function.
  1545. * @private
  1546. */
  1547. DataSet.prototype._sort = function (items, order) {
  1548. if (util.isString(order)) {
  1549. // order by provided field name
  1550. var name = order; // field name
  1551. items.sort(function (a, b) {
  1552. var av = a[name];
  1553. var bv = b[name];
  1554. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1555. });
  1556. }
  1557. else if (typeof order === 'function') {
  1558. // order by sort function
  1559. items.sort(order);
  1560. }
  1561. // TODO: extend order by an Object {field:String, direction:String}
  1562. // where direction can be 'asc' or 'desc'
  1563. else {
  1564. throw new TypeError('Order must be a function or a string');
  1565. }
  1566. };
  1567. /**
  1568. * Remove an object by pointer or by id
  1569. * @param {String | Number | Object | Array} id Object or id, or an array with
  1570. * objects or ids to be removed
  1571. * @param {String} [senderId] Optional sender id
  1572. */
  1573. DataSet.prototype.remove = function (id, senderId) {
  1574. var removedItems = [],
  1575. i, len;
  1576. if (util.isNumber(id) || util.isString(id)) {
  1577. delete this.data[id];
  1578. delete this.internalIds[id];
  1579. removedItems.push(id);
  1580. }
  1581. else if (id instanceof Array) {
  1582. for (i = 0, len = id.length; i < len; i++) {
  1583. this.remove(id[i]);
  1584. }
  1585. removedItems = items.concat(id);
  1586. }
  1587. else if (id instanceof Object) {
  1588. // search for the object
  1589. for (i in this.data) {
  1590. if (this.data.hasOwnProperty(i)) {
  1591. if (this.data[i] == id) {
  1592. delete this.data[i];
  1593. delete this.internalIds[i];
  1594. removedItems.push(i);
  1595. }
  1596. }
  1597. }
  1598. }
  1599. if (removedItems.length) {
  1600. this._trigger('remove', {items: removedItems}, senderId);
  1601. }
  1602. };
  1603. /**
  1604. * Clear the data
  1605. * @param {String} [senderId] Optional sender id
  1606. */
  1607. DataSet.prototype.clear = function (senderId) {
  1608. var ids = Object.keys(this.data);
  1609. this.data = {};
  1610. this.internalIds = {};
  1611. this._trigger('remove', {items: ids}, senderId);
  1612. };
  1613. /**
  1614. * Find the item with maximum value of a specified field
  1615. * @param {String} field
  1616. * @return {Object | null} item Item containing max value, or null if no items
  1617. */
  1618. DataSet.prototype.max = function (field) {
  1619. var data = this.data,
  1620. max = null,
  1621. maxField = null;
  1622. for (var id in data) {
  1623. if (data.hasOwnProperty(id)) {
  1624. var item = data[id];
  1625. var itemField = item[field];
  1626. if (itemField != null && (!max || itemField > maxField)) {
  1627. max = item;
  1628. maxField = itemField;
  1629. }
  1630. }
  1631. }
  1632. return max;
  1633. };
  1634. /**
  1635. * Find the item with minimum value of a specified field
  1636. * @param {String} field
  1637. * @return {Object | null} item Item containing max value, or null if no items
  1638. */
  1639. DataSet.prototype.min = function (field) {
  1640. var data = this.data,
  1641. min = null,
  1642. minField = null;
  1643. for (var id in data) {
  1644. if (data.hasOwnProperty(id)) {
  1645. var item = data[id];
  1646. var itemField = item[field];
  1647. if (itemField != null && (!min || itemField < minField)) {
  1648. min = item;
  1649. minField = itemField;
  1650. }
  1651. }
  1652. }
  1653. return min;
  1654. };
  1655. /**
  1656. * Find all distinct values of a specified field
  1657. * @param {String} field
  1658. * @return {Array} values Array containing all distinct values. If the data
  1659. * items do not contain the specified field, an array
  1660. * containing a single value undefined is returned.
  1661. * The returned array is unordered.
  1662. */
  1663. DataSet.prototype.distinct = function (field) {
  1664. var data = this.data,
  1665. values = [],
  1666. fieldType = this.options.fieldTypes[field],
  1667. count = 0;
  1668. for (var prop in data) {
  1669. if (data.hasOwnProperty(prop)) {
  1670. var item = data[prop];
  1671. var value = util.cast(item[field], fieldType);
  1672. var exists = false;
  1673. for (var i = 0; i < count; i++) {
  1674. if (values[i] == value) {
  1675. exists = true;
  1676. break;
  1677. }
  1678. }
  1679. if (!exists) {
  1680. values[count] = value;
  1681. count++;
  1682. }
  1683. }
  1684. }
  1685. return values;
  1686. };
  1687. /**
  1688. * Add a single item. Will fail when an item with the same id already exists.
  1689. * @param {Object} item
  1690. * @return {String} id
  1691. * @private
  1692. */
  1693. DataSet.prototype._addItem = function (item) {
  1694. var id = item[this.fieldId];
  1695. if (id != undefined) {
  1696. // check whether this id is already taken
  1697. if (this.data[id]) {
  1698. // item already exists
  1699. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1700. }
  1701. }
  1702. else {
  1703. // generate an id
  1704. id = util.randomUUID();
  1705. item[this.fieldId] = id;
  1706. this.internalIds[id] = item;
  1707. }
  1708. var d = {};
  1709. for (var field in item) {
  1710. if (item.hasOwnProperty(field)) {
  1711. var type = this.fieldTypes[field]; // type may be undefined
  1712. d[field] = util.cast(item[field], type);
  1713. }
  1714. }
  1715. this.data[id] = d;
  1716. return id;
  1717. };
  1718. /**
  1719. * Get an item. Fields can be casted to a specific type
  1720. * @param {String} id
  1721. * @param {Object.<String, String>} [fieldTypes] Cast field types
  1722. * @return {Object | null} item
  1723. * @private
  1724. */
  1725. DataSet.prototype._getItem = function (id, fieldTypes) {
  1726. var field, value;
  1727. // get the item from the dataset
  1728. var raw = this.data[id];
  1729. if (!raw) {
  1730. return null;
  1731. }
  1732. // cast the items field types
  1733. var casted = {},
  1734. fieldId = this.fieldId,
  1735. internalIds = this.internalIds;
  1736. if (fieldTypes) {
  1737. for (field in raw) {
  1738. if (raw.hasOwnProperty(field)) {
  1739. value = raw[field];
  1740. // output all fields, except internal ids
  1741. if ((field != fieldId) || !(value in internalIds)) {
  1742. casted[field] = util.cast(value, fieldTypes[field]);
  1743. }
  1744. }
  1745. }
  1746. }
  1747. else {
  1748. // no field types specified, no casting needed
  1749. for (field in raw) {
  1750. if (raw.hasOwnProperty(field)) {
  1751. value = raw[field];
  1752. // output all fields, except internal ids
  1753. if ((field != fieldId) || !(value in internalIds)) {
  1754. casted[field] = value;
  1755. }
  1756. }
  1757. }
  1758. }
  1759. return casted;
  1760. };
  1761. /**
  1762. * Update a single item: merge with existing item.
  1763. * Will fail when the item has no id, or when there does not exist an item
  1764. * with the same id.
  1765. * @param {Object} item
  1766. * @return {String} id
  1767. * @private
  1768. */
  1769. DataSet.prototype._updateItem = function (item) {
  1770. var id = item[this.fieldId];
  1771. if (id == undefined) {
  1772. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1773. }
  1774. var d = this.data[id];
  1775. if (!d) {
  1776. // item doesn't exist
  1777. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1778. }
  1779. // merge with current item
  1780. for (var field in item) {
  1781. if (item.hasOwnProperty(field)) {
  1782. var type = this.fieldTypes[field]; // type may be undefined
  1783. d[field] = util.cast(item[field], type);
  1784. }
  1785. }
  1786. return id;
  1787. };
  1788. /**
  1789. * Get an array with the column names of a Google DataTable
  1790. * @param {DataTable} dataTable
  1791. * @return {String[]} columnNames
  1792. * @private
  1793. */
  1794. DataSet.prototype._getColumnNames = function (dataTable) {
  1795. var columns = [];
  1796. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1797. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1798. }
  1799. return columns;
  1800. };
  1801. /**
  1802. * Append an item as a row to the dataTable
  1803. * @param dataTable
  1804. * @param columns
  1805. * @param item
  1806. * @private
  1807. */
  1808. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1809. var row = dataTable.addRow();
  1810. for (var col = 0, cols = columns.length; col < cols; col++) {
  1811. var field = columns[col];
  1812. dataTable.setValue(row, col, item[field]);
  1813. }
  1814. };
  1815. /**
  1816. * DataView
  1817. *
  1818. * a dataview offers a filtered view on a dataset or an other dataview.
  1819. *
  1820. * @param {DataSet | DataView} data
  1821. * @param {Object} [options] Available options: see method get
  1822. *
  1823. * @constructor DataView
  1824. */
  1825. function DataView (data, options) {
  1826. this.id = util.randomUUID();
  1827. this.data = null;
  1828. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1829. this.options = options || {};
  1830. this.fieldId = 'id'; // name of the field containing id
  1831. this.subscribers = {}; // event subscribers
  1832. var me = this;
  1833. this.listener = function () {
  1834. me._onEvent.apply(me, arguments);
  1835. };
  1836. this.setData(data);
  1837. }
  1838. /**
  1839. * Set a data source for the view
  1840. * @param {DataSet | DataView} data
  1841. */
  1842. DataView.prototype.setData = function (data) {
  1843. var ids, dataItems, i, len;
  1844. if (this.data) {
  1845. // unsubscribe from current dataset
  1846. if (this.data.unsubscribe) {
  1847. this.data.unsubscribe('*', this.listener);
  1848. }
  1849. // trigger a remove of all items in memory
  1850. ids = [];
  1851. for (var id in this.ids) {
  1852. if (this.ids.hasOwnProperty(id)) {
  1853. ids.push(id);
  1854. }
  1855. }
  1856. this.ids = {};
  1857. this._trigger('remove', {items: ids});
  1858. }
  1859. this.data = data;
  1860. if (this.data) {
  1861. // update fieldId
  1862. this.fieldId = this.options.fieldId ||
  1863. (this.data && this.data.options && this.data.options.fieldId) ||
  1864. 'id';
  1865. // trigger an add of all added items
  1866. ids = this.data.getIds({filter: this.options && this.options.filter});
  1867. for (i = 0, len = ids.length; i < len; i++) {
  1868. id = ids[i];
  1869. this.ids[id] = true;
  1870. }
  1871. this._trigger('add', {items: ids});
  1872. // subscribe to new dataset
  1873. if (this.data.subscribe) {
  1874. this.data.subscribe('*', this.listener);
  1875. }
  1876. }
  1877. };
  1878. /**
  1879. * Get data from the data view
  1880. *
  1881. * Usage:
  1882. *
  1883. * get()
  1884. * get(options: Object)
  1885. * get(options: Object, data: Array | DataTable)
  1886. *
  1887. * get(id: Number)
  1888. * get(id: Number, options: Object)
  1889. * get(id: Number, options: Object, data: Array | DataTable)
  1890. *
  1891. * get(ids: Number[])
  1892. * get(ids: Number[], options: Object)
  1893. * get(ids: Number[], options: Object, data: Array | DataTable)
  1894. *
  1895. * Where:
  1896. *
  1897. * {Number | String} id The id of an item
  1898. * {Number[] | String{}} ids An array with ids of items
  1899. * {Object} options An Object with options. Available options:
  1900. * {String} [type] Type of data to be returned. Can
  1901. * be 'DataTable' or 'Array' (default)
  1902. * {Object.<String, String>} [fieldTypes]
  1903. * {String[]} [fields] field names to be returned
  1904. * {function} [filter] filter items
  1905. * {String | function} [order] Order the items by
  1906. * a field name or custom sort function.
  1907. * {Array | DataTable} [data] If provided, items will be appended to this
  1908. * array or table. Required in case of Google
  1909. * DataTable.
  1910. * @param args
  1911. */
  1912. DataView.prototype.get = function (args) {
  1913. var me = this;
  1914. // parse the arguments
  1915. var ids, options, data;
  1916. var firstType = util.getType(arguments[0]);
  1917. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  1918. // get(id(s) [, options] [, data])
  1919. ids = arguments[0]; // can be a single id or an array with ids
  1920. options = arguments[1];
  1921. data = arguments[2];
  1922. }
  1923. else {
  1924. // get([, options] [, data])
  1925. options = arguments[0];
  1926. data = arguments[1];
  1927. }
  1928. // extend the options with the default options and provided options
  1929. var viewOptions = util.extend({}, this.options, options);
  1930. // create a combined filter method when needed
  1931. if (this.options.filter && options && options.filter) {
  1932. viewOptions.filter = function (item) {
  1933. return me.options.filter(item) && options.filter(item);
  1934. }
  1935. }
  1936. // build up the call to the linked data set
  1937. var getArguments = [];
  1938. if (ids != undefined) {
  1939. getArguments.push(ids);
  1940. }
  1941. getArguments.push(viewOptions);
  1942. getArguments.push(data);
  1943. return this.data && this.data.get.apply(this.data, getArguments);
  1944. };
  1945. /**
  1946. * Get ids of all items or from a filtered set of items.
  1947. * @param {Object} [options] An Object with options. Available options:
  1948. * {function} [filter] filter items
  1949. * {String | function} [order] Order the items by
  1950. * a field name or custom sort function.
  1951. * @return {Array} ids
  1952. */
  1953. DataView.prototype.getIds = function (options) {
  1954. var ids;
  1955. if (this.data) {
  1956. var defaultFilter = this.options.filter;
  1957. var filter;
  1958. if (options && options.filter) {
  1959. if (defaultFilter) {
  1960. filter = function (item) {
  1961. return defaultFilter(item) && options.filter(item);
  1962. }
  1963. }
  1964. else {
  1965. filter = options.filter;
  1966. }
  1967. }
  1968. else {
  1969. filter = defaultFilter;
  1970. }
  1971. ids = this.data.getIds({
  1972. filter: filter,
  1973. order: options && options.order
  1974. });
  1975. }
  1976. else {
  1977. ids = [];
  1978. }
  1979. return ids;
  1980. };
  1981. /**
  1982. * Event listener. Will propagate all events from the connected data set to
  1983. * the subscribers of the DataView, but will filter the items and only trigger
  1984. * when there are changes in the filtered data set.
  1985. * @param {String} event
  1986. * @param {Object | null} params
  1987. * @param {String} senderId
  1988. * @private
  1989. */
  1990. DataView.prototype._onEvent = function (event, params, senderId) {
  1991. var i, len, id, item,
  1992. ids = params && params.items,
  1993. data = this.data,
  1994. added = [],
  1995. updated = [],
  1996. removed = [];
  1997. if (ids && data) {
  1998. switch (event) {
  1999. case 'add':
  2000. // filter the ids of the added items
  2001. for (i = 0, len = ids.length; i < len; i++) {
  2002. id = ids[i];
  2003. item = this.get(id);
  2004. if (item) {
  2005. this.ids[id] = true;
  2006. added.push(id);
  2007. }
  2008. }
  2009. break;
  2010. case 'update':
  2011. // determine the event from the views viewpoint: an updated
  2012. // item can be added, updated, or removed from this view.
  2013. for (i = 0, len = ids.length; i < len; i++) {
  2014. id = ids[i];
  2015. item = this.get(id);
  2016. if (item) {
  2017. if (this.ids[id]) {
  2018. updated.push(id);
  2019. }
  2020. else {
  2021. this.ids[id] = true;
  2022. added.push(id);
  2023. }
  2024. }
  2025. else {
  2026. if (this.ids[id]) {
  2027. delete this.ids[id];
  2028. removed.push(id);
  2029. }
  2030. else {
  2031. // nothing interesting for me :-(
  2032. }
  2033. }
  2034. }
  2035. break;
  2036. case 'remove':
  2037. // filter the ids of the removed items
  2038. for (i = 0, len = ids.length; i < len; i++) {
  2039. id = ids[i];
  2040. if (this.ids[id]) {
  2041. delete this.ids[id];
  2042. removed.push(id);
  2043. }
  2044. }
  2045. break;
  2046. }
  2047. if (added.length) {
  2048. this._trigger('add', {items: added}, senderId);
  2049. }
  2050. if (updated.length) {
  2051. this._trigger('update', {items: updated}, senderId);
  2052. }
  2053. if (removed.length) {
  2054. this._trigger('remove', {items: removed}, senderId);
  2055. }
  2056. }
  2057. };
  2058. // copy subscription functionality from DataSet
  2059. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2060. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2061. DataView.prototype._trigger = DataSet.prototype._trigger;
  2062. /**
  2063. * @constructor TimeStep
  2064. * The class TimeStep is an iterator for dates. You provide a start date and an
  2065. * end date. The class itself determines the best scale (step size) based on the
  2066. * provided start Date, end Date, and minimumStep.
  2067. *
  2068. * If minimumStep is provided, the step size is chosen as close as possible
  2069. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2070. * provided, the scale is set to 1 DAY.
  2071. * The minimumStep should correspond with the onscreen size of about 6 characters
  2072. *
  2073. * Alternatively, you can set a scale by hand.
  2074. * After creation, you can initialize the class by executing first(). Then you
  2075. * can iterate from the start date to the end date via next(). You can check if
  2076. * the end date is reached with the function hasNext(). After each step, you can
  2077. * retrieve the current date via getCurrent().
  2078. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2079. * days, to years.
  2080. *
  2081. * Version: 1.2
  2082. *
  2083. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2084. * or new Date(2010, 9, 21, 23, 45, 00)
  2085. * @param {Date} [end] The end date
  2086. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2087. */
  2088. TimeStep = function(start, end, minimumStep) {
  2089. // variables
  2090. this.current = new Date();
  2091. this._start = new Date();
  2092. this._end = new Date();
  2093. this.autoScale = true;
  2094. this.scale = TimeStep.SCALE.DAY;
  2095. this.step = 1;
  2096. // initialize the range
  2097. this.setRange(start, end, minimumStep);
  2098. };
  2099. /// enum scale
  2100. TimeStep.SCALE = {
  2101. MILLISECOND: 1,
  2102. SECOND: 2,
  2103. MINUTE: 3,
  2104. HOUR: 4,
  2105. DAY: 5,
  2106. WEEKDAY: 6,
  2107. MONTH: 7,
  2108. YEAR: 8
  2109. };
  2110. /**
  2111. * Set a new range
  2112. * If minimumStep is provided, the step size is chosen as close as possible
  2113. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2114. * provided, the scale is set to 1 DAY.
  2115. * The minimumStep should correspond with the onscreen size of about 6 characters
  2116. * @param {Date} [start] The start date and time.
  2117. * @param {Date} [end] The end date and time.
  2118. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2119. */
  2120. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2121. if (!(start instanceof Date) || !(end instanceof Date)) {
  2122. //throw "No legal start or end date in method setRange";
  2123. return;
  2124. }
  2125. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2126. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2127. if (this.autoScale) {
  2128. this.setMinimumStep(minimumStep);
  2129. }
  2130. };
  2131. /**
  2132. * Set the range iterator to the start date.
  2133. */
  2134. TimeStep.prototype.first = function() {
  2135. this.current = new Date(this._start.valueOf());
  2136. this.roundToMinor();
  2137. };
  2138. /**
  2139. * Round the current date to the first minor date value
  2140. * This must be executed once when the current date is set to start Date
  2141. */
  2142. TimeStep.prototype.roundToMinor = function() {
  2143. // round to floor
  2144. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2145. //noinspection FallthroughInSwitchStatementJS
  2146. switch (this.scale) {
  2147. case TimeStep.SCALE.YEAR:
  2148. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2149. this.current.setMonth(0);
  2150. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2151. case TimeStep.SCALE.DAY: // intentional fall through
  2152. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2153. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2154. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2155. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2156. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2157. }
  2158. if (this.step != 1) {
  2159. // round down to the first minor value that is a multiple of the current step size
  2160. switch (this.scale) {
  2161. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2162. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2163. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2164. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2165. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2166. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2167. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2168. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2169. default: break;
  2170. }
  2171. }
  2172. };
  2173. /**
  2174. * Check if the there is a next step
  2175. * @return {boolean} true if the current date has not passed the end date
  2176. */
  2177. TimeStep.prototype.hasNext = function () {
  2178. return (this.current.valueOf() <= this._end.valueOf());
  2179. };
  2180. /**
  2181. * Do the next step
  2182. */
  2183. TimeStep.prototype.next = function() {
  2184. var prev = this.current.valueOf();
  2185. // Two cases, needed to prevent issues with switching daylight savings
  2186. // (end of March and end of October)
  2187. if (this.current.getMonth() < 6) {
  2188. switch (this.scale) {
  2189. case TimeStep.SCALE.MILLISECOND:
  2190. this.current = new Date(this.current.valueOf() + this.step); break;
  2191. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2192. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2193. case TimeStep.SCALE.HOUR:
  2194. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2195. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2196. var h = this.current.getHours();
  2197. this.current.setHours(h - (h % this.step));
  2198. break;
  2199. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2200. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2201. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2202. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2203. default: break;
  2204. }
  2205. }
  2206. else {
  2207. switch (this.scale) {
  2208. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2209. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2210. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2211. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2212. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2213. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2214. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2215. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2216. default: break;
  2217. }
  2218. }
  2219. if (this.step != 1) {
  2220. // round down to the correct major value
  2221. switch (this.scale) {
  2222. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2223. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2224. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2225. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2226. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2227. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2228. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2229. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2230. default: break;
  2231. }
  2232. }
  2233. // safety mechanism: if current time is still unchanged, move to the end
  2234. if (this.current.valueOf() == prev) {
  2235. this.current = new Date(this._end.valueOf());
  2236. }
  2237. };
  2238. /**
  2239. * Get the current datetime
  2240. * @return {Date} current The current date
  2241. */
  2242. TimeStep.prototype.getCurrent = function() {
  2243. return this.current;
  2244. };
  2245. /**
  2246. * Set a custom scale. Autoscaling will be disabled.
  2247. * For example setScale(SCALE.MINUTES, 5) will result
  2248. * in minor steps of 5 minutes, and major steps of an hour.
  2249. *
  2250. * @param {TimeStep.SCALE} newScale
  2251. * A scale. Choose from SCALE.MILLISECOND,
  2252. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2253. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2254. * SCALE.YEAR.
  2255. * @param {Number} newStep A step size, by default 1. Choose for
  2256. * example 1, 2, 5, or 10.
  2257. */
  2258. TimeStep.prototype.setScale = function(newScale, newStep) {
  2259. this.scale = newScale;
  2260. if (newStep > 0) {
  2261. this.step = newStep;
  2262. }
  2263. this.autoScale = false;
  2264. };
  2265. /**
  2266. * Enable or disable autoscaling
  2267. * @param {boolean} enable If true, autoascaling is set true
  2268. */
  2269. TimeStep.prototype.setAutoScale = function (enable) {
  2270. this.autoScale = enable;
  2271. };
  2272. /**
  2273. * Automatically determine the scale that bests fits the provided minimum step
  2274. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2275. */
  2276. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2277. if (minimumStep == undefined) {
  2278. return;
  2279. }
  2280. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2281. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2282. var stepDay = (1000 * 60 * 60 * 24);
  2283. var stepHour = (1000 * 60 * 60);
  2284. var stepMinute = (1000 * 60);
  2285. var stepSecond = (1000);
  2286. var stepMillisecond= (1);
  2287. // find the smallest step that is larger than the provided minimumStep
  2288. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2289. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2290. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2291. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2292. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2293. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2294. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2295. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2296. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2297. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2298. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2299. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2300. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2301. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2302. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2303. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2304. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2305. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2306. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2307. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2308. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2309. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2310. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2311. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2312. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2313. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2314. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2315. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2316. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2317. };
  2318. /**
  2319. * Snap a date to a rounded value. The snap intervals are dependent on the
  2320. * current scale and step.
  2321. * @param {Date} date the date to be snapped
  2322. */
  2323. TimeStep.prototype.snap = function(date) {
  2324. if (this.scale == TimeStep.SCALE.YEAR) {
  2325. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2326. date.setFullYear(Math.round(year / this.step) * this.step);
  2327. date.setMonth(0);
  2328. date.setDate(0);
  2329. date.setHours(0);
  2330. date.setMinutes(0);
  2331. date.setSeconds(0);
  2332. date.setMilliseconds(0);
  2333. }
  2334. else if (this.scale == TimeStep.SCALE.MONTH) {
  2335. if (date.getDate() > 15) {
  2336. date.setDate(1);
  2337. date.setMonth(date.getMonth() + 1);
  2338. // important: first set Date to 1, after that change the month.
  2339. }
  2340. else {
  2341. date.setDate(1);
  2342. }
  2343. date.setHours(0);
  2344. date.setMinutes(0);
  2345. date.setSeconds(0);
  2346. date.setMilliseconds(0);
  2347. }
  2348. else if (this.scale == TimeStep.SCALE.DAY ||
  2349. this.scale == TimeStep.SCALE.WEEKDAY) {
  2350. //noinspection FallthroughInSwitchStatementJS
  2351. switch (this.step) {
  2352. case 5:
  2353. case 2:
  2354. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2355. default:
  2356. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2357. }
  2358. date.setMinutes(0);
  2359. date.setSeconds(0);
  2360. date.setMilliseconds(0);
  2361. }
  2362. else if (this.scale == TimeStep.SCALE.HOUR) {
  2363. switch (this.step) {
  2364. case 4:
  2365. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2366. default:
  2367. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2368. }
  2369. date.setSeconds(0);
  2370. date.setMilliseconds(0);
  2371. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2372. //noinspection FallthroughInSwitchStatementJS
  2373. switch (this.step) {
  2374. case 15:
  2375. case 10:
  2376. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2377. date.setSeconds(0);
  2378. break;
  2379. case 5:
  2380. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2381. default:
  2382. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2383. }
  2384. date.setMilliseconds(0);
  2385. }
  2386. else if (this.scale == TimeStep.SCALE.SECOND) {
  2387. //noinspection FallthroughInSwitchStatementJS
  2388. switch (this.step) {
  2389. case 15:
  2390. case 10:
  2391. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2392. date.setMilliseconds(0);
  2393. break;
  2394. case 5:
  2395. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2396. default:
  2397. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2398. }
  2399. }
  2400. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2401. var step = this.step > 5 ? this.step / 2 : 1;
  2402. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2403. }
  2404. };
  2405. /**
  2406. * Check if the current value is a major value (for example when the step
  2407. * is DAY, a major value is each first day of the MONTH)
  2408. * @return {boolean} true if current date is major, else false.
  2409. */
  2410. TimeStep.prototype.isMajor = function() {
  2411. switch (this.scale) {
  2412. case TimeStep.SCALE.MILLISECOND:
  2413. return (this.current.getMilliseconds() == 0);
  2414. case TimeStep.SCALE.SECOND:
  2415. return (this.current.getSeconds() == 0);
  2416. case TimeStep.SCALE.MINUTE:
  2417. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2418. // Note: this is no bug. Major label is equal for both minute and hour scale
  2419. case TimeStep.SCALE.HOUR:
  2420. return (this.current.getHours() == 0);
  2421. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2422. case TimeStep.SCALE.DAY:
  2423. return (this.current.getDate() == 1);
  2424. case TimeStep.SCALE.MONTH:
  2425. return (this.current.getMonth() == 0);
  2426. case TimeStep.SCALE.YEAR:
  2427. return false;
  2428. default:
  2429. return false;
  2430. }
  2431. };
  2432. /**
  2433. * Returns formatted text for the minor axislabel, depending on the current
  2434. * date and the scale. For example when scale is MINUTE, the current time is
  2435. * formatted as "hh:mm".
  2436. * @param {Date} [date] custom date. if not provided, current date is taken
  2437. */
  2438. TimeStep.prototype.getLabelMinor = function(date) {
  2439. if (date == undefined) {
  2440. date = this.current;
  2441. }
  2442. switch (this.scale) {
  2443. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2444. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2445. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2446. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2447. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2448. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2449. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2450. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2451. default: return '';
  2452. }
  2453. };
  2454. /**
  2455. * Returns formatted text for the major axis label, depending on the current
  2456. * date and the scale. For example when scale is MINUTE, the major scale is
  2457. * hours, and the hour will be formatted as "hh".
  2458. * @param {Date} [date] custom date. if not provided, current date is taken
  2459. */
  2460. TimeStep.prototype.getLabelMajor = function(date) {
  2461. if (date == undefined) {
  2462. date = this.current;
  2463. }
  2464. //noinspection FallthroughInSwitchStatementJS
  2465. switch (this.scale) {
  2466. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2467. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2468. case TimeStep.SCALE.MINUTE:
  2469. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2470. case TimeStep.SCALE.WEEKDAY:
  2471. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2472. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2473. case TimeStep.SCALE.YEAR: return '';
  2474. default: return '';
  2475. }
  2476. };
  2477. /**
  2478. * @constructor Stack
  2479. * Stacks items on top of each other.
  2480. * @param {ItemSet} parent
  2481. * @param {Object} [options]
  2482. */
  2483. function Stack (parent, options) {
  2484. this.parent = parent;
  2485. this.options = options || {};
  2486. this.defaultOptions = {
  2487. order: function (a, b) {
  2488. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2489. // Order: ranges over non-ranges, ranged ordered by width, and
  2490. // lastly ordered by start.
  2491. if (a instanceof ItemRange) {
  2492. if (b instanceof ItemRange) {
  2493. var aInt = (a.data.end - a.data.start);
  2494. var bInt = (b.data.end - b.data.start);
  2495. return (aInt - bInt) || (a.data.start - b.data.start);
  2496. }
  2497. else {
  2498. return -1;
  2499. }
  2500. }
  2501. else {
  2502. if (b instanceof ItemRange) {
  2503. return 1;
  2504. }
  2505. else {
  2506. return (a.data.start - b.data.start);
  2507. }
  2508. }
  2509. },
  2510. margin: {
  2511. item: 10
  2512. }
  2513. };
  2514. this.ordered = []; // ordered items
  2515. }
  2516. /**
  2517. * Set options for the stack
  2518. * @param {Object} options Available options:
  2519. * {ItemSet} parent
  2520. * {Number} margin
  2521. * {function} order Stacking order
  2522. */
  2523. Stack.prototype.setOptions = function setOptions (options) {
  2524. util.extend(this.options, options);
  2525. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2526. };
  2527. /**
  2528. * Stack the items such that they don't overlap. The items will have a minimal
  2529. * distance equal to options.margin.item.
  2530. */
  2531. Stack.prototype.update = function update() {
  2532. this._order();
  2533. this._stack();
  2534. };
  2535. /**
  2536. * Order the items. The items are ordered by width first, and by left position
  2537. * second.
  2538. * If a custom order function has been provided via the options, then this will
  2539. * be used.
  2540. * @private
  2541. */
  2542. Stack.prototype._order = function _order () {
  2543. var items = this.parent.items;
  2544. if (!items) {
  2545. throw new Error('Cannot stack items: parent does not contain items');
  2546. }
  2547. // TODO: store the sorted items, to have less work later on
  2548. var ordered = [];
  2549. var index = 0;
  2550. // items is a map (no array)
  2551. util.forEach(items, function (item) {
  2552. if (item.visible) {
  2553. ordered[index] = item;
  2554. index++;
  2555. }
  2556. });
  2557. //if a customer stack order function exists, use it.
  2558. var order = this.options.order || this.defaultOptions.order;
  2559. if (!(typeof order === 'function')) {
  2560. throw new Error('Option order must be a function');
  2561. }
  2562. ordered.sort(order);
  2563. this.ordered = ordered;
  2564. };
  2565. /**
  2566. * Adjust vertical positions of the events such that they don't overlap each
  2567. * other.
  2568. * @private
  2569. */
  2570. Stack.prototype._stack = function _stack () {
  2571. var i,
  2572. iMax,
  2573. ordered = this.ordered,
  2574. options = this.options,
  2575. orientation = options.orientation || this.defaultOptions.orientation,
  2576. axisOnTop = (orientation == 'top'),
  2577. margin;
  2578. if (options.margin && options.margin.item !== undefined) {
  2579. margin = options.margin.item;
  2580. }
  2581. else {
  2582. margin = this.defaultOptions.margin.item
  2583. }
  2584. // calculate new, non-overlapping positions
  2585. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2586. var item = ordered[i];
  2587. var collidingItem = null;
  2588. do {
  2589. // TODO: optimize checking for overlap. when there is a gap without items,
  2590. // you only need to check for items from the next item on, not from zero
  2591. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2592. if (collidingItem != null) {
  2593. // There is a collision. Reposition the event above the colliding element
  2594. if (axisOnTop) {
  2595. item.top = collidingItem.top + collidingItem.height + margin;
  2596. }
  2597. else {
  2598. item.top = collidingItem.top - item.height - margin;
  2599. }
  2600. }
  2601. } while (collidingItem);
  2602. }
  2603. };
  2604. /**
  2605. * Check if the destiny position of given item overlaps with any
  2606. * of the other items from index itemStart to itemEnd.
  2607. * @param {Array} items Array with items
  2608. * @param {int} itemIndex Number of the item to be checked for overlap
  2609. * @param {int} itemStart First item to be checked.
  2610. * @param {int} itemEnd Last item to be checked.
  2611. * @return {Object | null} colliding item, or undefined when no collisions
  2612. * @param {Number} margin A minimum required margin.
  2613. * If margin is provided, the two items will be
  2614. * marked colliding when they overlap or
  2615. * when the margin between the two is smaller than
  2616. * the requested margin.
  2617. */
  2618. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2619. itemStart, itemEnd, margin) {
  2620. var collision = this.collision;
  2621. // we loop from end to start, as we suppose that the chance of a
  2622. // collision is larger for items at the end, so check these first.
  2623. var a = items[itemIndex];
  2624. for (var i = itemEnd; i >= itemStart; i--) {
  2625. var b = items[i];
  2626. if (collision(a, b, margin)) {
  2627. if (i != itemIndex) {
  2628. return b;
  2629. }
  2630. }
  2631. }
  2632. return null;
  2633. };
  2634. /**
  2635. * Test if the two provided items collide
  2636. * The items must have parameters left, width, top, and height.
  2637. * @param {Component} a The first item
  2638. * @param {Component} b The second item
  2639. * @param {Number} margin A minimum required margin.
  2640. * If margin is provided, the two items will be
  2641. * marked colliding when they overlap or
  2642. * when the margin between the two is smaller than
  2643. * the requested margin.
  2644. * @return {boolean} true if a and b collide, else false
  2645. */
  2646. Stack.prototype.collision = function collision (a, b, margin) {
  2647. return ((a.left - margin) < (b.left + b.width) &&
  2648. (a.left + a.width + margin) > b.left &&
  2649. (a.top - margin) < (b.top + b.height) &&
  2650. (a.top + a.height + margin) > b.top);
  2651. };
  2652. /**
  2653. * @constructor Range
  2654. * A Range controls a numeric range with a start and end value.
  2655. * The Range adjusts the range based on mouse events or programmatic changes,
  2656. * and triggers events when the range is changing or has been changed.
  2657. * @param {Object} [options] See description at Range.setOptions
  2658. * @extends Controller
  2659. */
  2660. function Range(options) {
  2661. this.id = util.randomUUID();
  2662. this.start = 0; // Number
  2663. this.end = 0; // Number
  2664. this.options = {
  2665. min: null,
  2666. max: null,
  2667. zoomMin: null,
  2668. zoomMax: null
  2669. };
  2670. this.listeners = [];
  2671. this.setOptions(options);
  2672. }
  2673. /**
  2674. * Set options for the range controller
  2675. * @param {Object} options Available options:
  2676. * {Number} start Set start value of the range
  2677. * {Number} end Set end value of the range
  2678. * {Number} min Minimum value for start
  2679. * {Number} max Maximum value for end
  2680. * {Number} zoomMin Set a minimum value for
  2681. * (end - start).
  2682. * {Number} zoomMax Set a maximum value for
  2683. * (end - start).
  2684. */
  2685. Range.prototype.setOptions = function (options) {
  2686. util.extend(this.options, options);
  2687. if (options.start != null || options.end != null) {
  2688. this.setRange(options.start, options.end);
  2689. }
  2690. };
  2691. /**
  2692. * Add listeners for mouse and touch events to the component
  2693. * @param {Component} component
  2694. * @param {String} event Available events: 'move', 'zoom'
  2695. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2696. */
  2697. Range.prototype.subscribe = function (component, event, direction) {
  2698. var me = this;
  2699. var listener;
  2700. if (direction != 'horizontal' && direction != 'vertical') {
  2701. throw new TypeError('Unknown direction "' + direction + '". ' +
  2702. 'Choose "horizontal" or "vertical".');
  2703. }
  2704. //noinspection FallthroughInSwitchStatementJS
  2705. if (event == 'move') {
  2706. listener = {
  2707. component: component,
  2708. event: event,
  2709. direction: direction,
  2710. callback: function (event) {
  2711. me._onMouseDown(event, listener);
  2712. },
  2713. params: {}
  2714. };
  2715. component.on('mousedown', listener.callback);
  2716. me.listeners.push(listener);
  2717. }
  2718. else if (event == 'zoom') {
  2719. listener = {
  2720. component: component,
  2721. event: event,
  2722. direction: direction,
  2723. callback: function (event) {
  2724. me._onMouseWheel(event, listener);
  2725. },
  2726. params: {}
  2727. };
  2728. component.on('mousewheel', listener.callback);
  2729. me.listeners.push(listener);
  2730. }
  2731. else {
  2732. throw new TypeError('Unknown event "' + event + '". ' +
  2733. 'Choose "move" or "zoom".');
  2734. }
  2735. };
  2736. /**
  2737. * Event handler
  2738. * @param {String} event name of the event, for example 'click', 'mousemove'
  2739. * @param {function} callback callback handler, invoked with the raw HTML Event
  2740. * as parameter.
  2741. */
  2742. Range.prototype.on = function (event, callback) {
  2743. events.addListener(this, event, callback);
  2744. };
  2745. /**
  2746. * Trigger an event
  2747. * @param {String} event name of the event, available events: 'rangechange',
  2748. * 'rangechanged'
  2749. * @private
  2750. */
  2751. Range.prototype._trigger = function (event) {
  2752. events.trigger(this, event, {
  2753. start: this.start,
  2754. end: this.end
  2755. });
  2756. };
  2757. /**
  2758. * Set a new start and end range
  2759. * @param {Number} start
  2760. * @param {Number} end
  2761. */
  2762. Range.prototype.setRange = function(start, end) {
  2763. var changed = this._applyRange(start, end);
  2764. if (changed) {
  2765. this._trigger('rangechange');
  2766. this._trigger('rangechanged');
  2767. }
  2768. };
  2769. /**
  2770. * Set a new start and end range. This method is the same as setRange, but
  2771. * does not trigger a range change and range changed event, and it returns
  2772. * true when the range is changed
  2773. * @param {Number} start
  2774. * @param {Number} end
  2775. * @return {Boolean} changed
  2776. * @private
  2777. */
  2778. Range.prototype._applyRange = function(start, end) {
  2779. var newStart = (start != null) ? util.cast(start, 'Number') : this.start;
  2780. var newEnd = (end != null) ? util.cast(end, 'Number') : this.end;
  2781. var diff;
  2782. // check for valid number
  2783. if (isNaN(newStart)) {
  2784. throw new Error('Invalid start "' + start + '"');
  2785. }
  2786. if (isNaN(newEnd)) {
  2787. throw new Error('Invalid end "' + end + '"');
  2788. }
  2789. // prevent start < end
  2790. if (newEnd < newStart) {
  2791. newEnd = newStart;
  2792. }
  2793. // prevent start < min
  2794. if (this.options.min != null) {
  2795. var min = this.options.min.valueOf();
  2796. if (newStart < min) {
  2797. diff = (min - newStart);
  2798. newStart += diff;
  2799. newEnd += diff;
  2800. }
  2801. }
  2802. // prevent end > max
  2803. if (this.options.max != null) {
  2804. var max = this.options.max.valueOf();
  2805. if (newEnd > max) {
  2806. diff = (newEnd - max);
  2807. newStart -= diff;
  2808. newEnd -= diff;
  2809. }
  2810. }
  2811. // prevent (end-start) > zoomMin
  2812. if (this.options.zoomMin != null) {
  2813. var zoomMin = this.options.zoomMin.valueOf();
  2814. if (zoomMin < 0) {
  2815. zoomMin = 0;
  2816. }
  2817. if ((newEnd - newStart) < zoomMin) {
  2818. if ((this.end - this.start) > zoomMin) {
  2819. // zoom to the minimum
  2820. diff = (zoomMin - (newEnd - newStart));
  2821. newStart -= diff / 2;
  2822. newEnd += diff / 2;
  2823. }
  2824. else {
  2825. // ingore this action, we are already zoomed to the minimum
  2826. newStart = this.start;
  2827. newEnd = this.end;
  2828. }
  2829. }
  2830. }
  2831. // prevent (end-start) > zoomMin
  2832. if (this.options.zoomMax != null) {
  2833. var zoomMax = this.options.zoomMax.valueOf();
  2834. if (zoomMax < 0) {
  2835. zoomMax = 0;
  2836. }
  2837. if ((newEnd - newStart) > zoomMax) {
  2838. if ((this.end - this.start) < zoomMax) {
  2839. // zoom to the maximum
  2840. diff = ((newEnd - newStart) - zoomMax);
  2841. newStart += diff / 2;
  2842. newEnd -= diff / 2;
  2843. }
  2844. else {
  2845. // ingore this action, we are already zoomed to the maximum
  2846. newStart = this.start;
  2847. newEnd = this.end;
  2848. }
  2849. }
  2850. }
  2851. var changed = (this.start != newStart || this.end != newEnd);
  2852. this.start = newStart;
  2853. this.end = newEnd;
  2854. return changed;
  2855. };
  2856. /**
  2857. * Retrieve the current range.
  2858. * @return {Object} An object with start and end properties
  2859. */
  2860. Range.prototype.getRange = function() {
  2861. return {
  2862. start: this.start,
  2863. end: this.end
  2864. };
  2865. };
  2866. /**
  2867. * Calculate the conversion offset and factor for current range, based on
  2868. * the provided width
  2869. * @param {Number} width
  2870. * @returns {{offset: number, factor: number}} conversion
  2871. */
  2872. Range.prototype.conversion = function (width) {
  2873. var start = this.start;
  2874. var end = this.end;
  2875. return Range.conversion(this.start, this.end, width);
  2876. };
  2877. /**
  2878. * Static method to calculate the conversion offset and factor for a range,
  2879. * based on the provided start, end, and width
  2880. * @param {Number} start
  2881. * @param {Number} end
  2882. * @param {Number} width
  2883. * @returns {{offset: number, factor: number}} conversion
  2884. */
  2885. Range.conversion = function (start, end, width) {
  2886. if (width != 0 && (end - start != 0)) {
  2887. return {
  2888. offset: start,
  2889. factor: width / (end - start)
  2890. }
  2891. }
  2892. else {
  2893. return {
  2894. offset: 0,
  2895. factor: 1
  2896. };
  2897. }
  2898. };
  2899. /**
  2900. * Start moving horizontally or vertically
  2901. * @param {Event} event
  2902. * @param {Object} listener Listener containing the component and params
  2903. * @private
  2904. */
  2905. Range.prototype._onMouseDown = function(event, listener) {
  2906. event = event || window.event;
  2907. var params = listener.params;
  2908. // only react on left mouse button down
  2909. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2910. if (!leftButtonDown) {
  2911. return;
  2912. }
  2913. // get mouse position
  2914. params.mouseX = util.getPageX(event);
  2915. params.mouseY = util.getPageY(event);
  2916. params.previousLeft = 0;
  2917. params.previousOffset = 0;
  2918. params.moved = false;
  2919. params.start = this.start;
  2920. params.end = this.end;
  2921. var frame = listener.component.frame;
  2922. if (frame) {
  2923. frame.style.cursor = 'move';
  2924. }
  2925. // add event listeners to handle moving the contents
  2926. // we store the function onmousemove and onmouseup in the timeaxis,
  2927. // so we can remove the eventlisteners lateron in the function onmouseup
  2928. var me = this;
  2929. if (!params.onMouseMove) {
  2930. params.onMouseMove = function (event) {
  2931. me._onMouseMove(event, listener);
  2932. };
  2933. util.addEventListener(document, "mousemove", params.onMouseMove);
  2934. }
  2935. if (!params.onMouseUp) {
  2936. params.onMouseUp = function (event) {
  2937. me._onMouseUp(event, listener);
  2938. };
  2939. util.addEventListener(document, "mouseup", params.onMouseUp);
  2940. }
  2941. util.preventDefault(event);
  2942. };
  2943. /**
  2944. * Perform moving operating.
  2945. * This function activated from within the funcion TimeAxis._onMouseDown().
  2946. * @param {Event} event
  2947. * @param {Object} listener
  2948. * @private
  2949. */
  2950. Range.prototype._onMouseMove = function (event, listener) {
  2951. event = event || window.event;
  2952. var params = listener.params;
  2953. // calculate change in mouse position
  2954. var mouseX = util.getPageX(event);
  2955. var mouseY = util.getPageY(event);
  2956. if (params.mouseX == undefined) {
  2957. params.mouseX = mouseX;
  2958. }
  2959. if (params.mouseY == undefined) {
  2960. params.mouseY = mouseY;
  2961. }
  2962. var diffX = mouseX - params.mouseX;
  2963. var diffY = mouseY - params.mouseY;
  2964. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  2965. // if mouse movement is big enough, register it as a "moved" event
  2966. if (Math.abs(diff) >= 1) {
  2967. params.moved = true;
  2968. }
  2969. var interval = (params.end - params.start);
  2970. var width = (listener.direction == 'horizontal') ?
  2971. listener.component.width : listener.component.height;
  2972. var diffRange = -diff / width * interval;
  2973. this._applyRange(params.start + diffRange, params.end + diffRange);
  2974. // fire a rangechange event
  2975. this._trigger('rangechange');
  2976. util.preventDefault(event);
  2977. };
  2978. /**
  2979. * Stop moving operating.
  2980. * This function activated from within the function Range._onMouseDown().
  2981. * @param {event} event
  2982. * @param {Object} listener
  2983. * @private
  2984. */
  2985. Range.prototype._onMouseUp = function (event, listener) {
  2986. event = event || window.event;
  2987. var params = listener.params;
  2988. if (listener.component.frame) {
  2989. listener.component.frame.style.cursor = 'auto';
  2990. }
  2991. // remove event listeners here, important for Safari
  2992. if (params.onMouseMove) {
  2993. util.removeEventListener(document, "mousemove", params.onMouseMove);
  2994. params.onMouseMove = null;
  2995. }
  2996. if (params.onMouseUp) {
  2997. util.removeEventListener(document, "mouseup", params.onMouseUp);
  2998. params.onMouseUp = null;
  2999. }
  3000. //util.preventDefault(event);
  3001. if (params.moved) {
  3002. // fire a rangechanged event
  3003. this._trigger('rangechanged');
  3004. }
  3005. };
  3006. /**
  3007. * Event handler for mouse wheel event, used to zoom
  3008. * Code from http://adomas.org/javascript-mouse-wheel/
  3009. * @param {Event} event
  3010. * @param {Object} listener
  3011. * @private
  3012. */
  3013. Range.prototype._onMouseWheel = function(event, listener) {
  3014. event = event || window.event;
  3015. // retrieve delta
  3016. var delta = 0;
  3017. if (event.wheelDelta) { /* IE/Opera. */
  3018. delta = event.wheelDelta / 120;
  3019. } else if (event.detail) { /* Mozilla case. */
  3020. // In Mozilla, sign of delta is different than in IE.
  3021. // Also, delta is multiple of 3.
  3022. delta = -event.detail / 3;
  3023. }
  3024. // If delta is nonzero, handle it.
  3025. // Basically, delta is now positive if wheel was scrolled up,
  3026. // and negative, if wheel was scrolled down.
  3027. if (delta) {
  3028. var me = this;
  3029. var zoom = function () {
  3030. // perform the zoom action. Delta is normally 1 or -1
  3031. var zoomFactor = delta / 5.0;
  3032. var zoomAround = null;
  3033. var frame = listener.component.frame;
  3034. if (frame) {
  3035. var size, conversion;
  3036. if (listener.direction == 'horizontal') {
  3037. size = listener.component.width;
  3038. conversion = me.conversion(size);
  3039. var frameLeft = util.getAbsoluteLeft(frame);
  3040. var mouseX = util.getPageX(event);
  3041. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  3042. }
  3043. else {
  3044. size = listener.component.height;
  3045. conversion = me.conversion(size);
  3046. var frameTop = util.getAbsoluteTop(frame);
  3047. var mouseY = util.getPageY(event);
  3048. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  3049. }
  3050. }
  3051. me.zoom(zoomFactor, zoomAround);
  3052. };
  3053. zoom();
  3054. }
  3055. // Prevent default actions caused by mouse wheel.
  3056. // That might be ugly, but we handle scrolls somehow
  3057. // anyway, so don't bother here...
  3058. util.preventDefault(event);
  3059. };
  3060. /**
  3061. * Zoom the range the given zoomfactor in or out. Start and end date will
  3062. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3063. * date around which to zoom.
  3064. * For example, try zoomfactor = 0.1 or -0.1
  3065. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  3066. * negative value will zoom out
  3067. * @param {Number} zoomAround Value around which will be zoomed. Optional
  3068. */
  3069. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  3070. // if zoomAroundDate is not provided, take it half between start Date and end Date
  3071. if (zoomAround == null) {
  3072. zoomAround = (this.start + this.end) / 2;
  3073. }
  3074. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  3075. // result in a start>=end )
  3076. if (zoomFactor >= 1) {
  3077. zoomFactor = 0.9;
  3078. }
  3079. if (zoomFactor <= -1) {
  3080. zoomFactor = -0.9;
  3081. }
  3082. // adjust a negative factor such that zooming in with 0.1 equals zooming
  3083. // out with a factor -0.1
  3084. if (zoomFactor < 0) {
  3085. zoomFactor = zoomFactor / (1 + zoomFactor);
  3086. }
  3087. // zoom start and end relative to the zoomAround value
  3088. var startDiff = (this.start - zoomAround);
  3089. var endDiff = (this.end - zoomAround);
  3090. // calculate new start and end
  3091. var newStart = this.start - startDiff * zoomFactor;
  3092. var newEnd = this.end - endDiff * zoomFactor;
  3093. this.setRange(newStart, newEnd);
  3094. };
  3095. /**
  3096. * Move the range with a given factor to the left or right. Start and end
  3097. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  3098. * @param {Number} moveFactor Moving amount. Positive value will move right,
  3099. * negative value will move left
  3100. */
  3101. Range.prototype.move = function(moveFactor) {
  3102. // zoom start Date and end Date relative to the zoomAroundDate
  3103. var diff = (this.end - this.start);
  3104. // apply new values
  3105. var newStart = this.start + diff * moveFactor;
  3106. var newEnd = this.end + diff * moveFactor;
  3107. // TODO: reckon with min and max range
  3108. this.start = newStart;
  3109. this.end = newEnd;
  3110. };
  3111. /**
  3112. * @constructor Controller
  3113. *
  3114. * A Controller controls the reflows and repaints of all visual components
  3115. */
  3116. function Controller () {
  3117. this.id = util.randomUUID();
  3118. this.components = {};
  3119. this.repaintTimer = undefined;
  3120. this.reflowTimer = undefined;
  3121. }
  3122. /**
  3123. * Add a component to the controller
  3124. * @param {Component} component
  3125. */
  3126. Controller.prototype.add = function add(component) {
  3127. // validate the component
  3128. if (component.id == undefined) {
  3129. throw new Error('Component has no field id');
  3130. }
  3131. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3132. throw new TypeError('Component must be an instance of ' +
  3133. 'prototype Component or Controller');
  3134. }
  3135. // add the component
  3136. component.controller = this;
  3137. this.components[component.id] = component;
  3138. };
  3139. /**
  3140. * Remove a component from the controller
  3141. * @param {Component | String} component
  3142. */
  3143. Controller.prototype.remove = function remove(component) {
  3144. var id;
  3145. for (id in this.components) {
  3146. if (this.components.hasOwnProperty(id)) {
  3147. if (id == component || this.components[id] == component) {
  3148. break;
  3149. }
  3150. }
  3151. }
  3152. if (id) {
  3153. delete this.components[id];
  3154. }
  3155. };
  3156. /**
  3157. * Request a reflow. The controller will schedule a reflow
  3158. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3159. * is false.
  3160. */
  3161. Controller.prototype.requestReflow = function requestReflow(force) {
  3162. if (force) {
  3163. this.reflow();
  3164. }
  3165. else {
  3166. if (!this.reflowTimer) {
  3167. var me = this;
  3168. this.reflowTimer = setTimeout(function () {
  3169. me.reflowTimer = undefined;
  3170. me.reflow();
  3171. }, 0);
  3172. }
  3173. }
  3174. };
  3175. /**
  3176. * Request a repaint. The controller will schedule a repaint
  3177. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3178. * is false.
  3179. */
  3180. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3181. if (force) {
  3182. this.repaint();
  3183. }
  3184. else {
  3185. if (!this.repaintTimer) {
  3186. var me = this;
  3187. this.repaintTimer = setTimeout(function () {
  3188. me.repaintTimer = undefined;
  3189. me.repaint();
  3190. }, 0);
  3191. }
  3192. }
  3193. };
  3194. /**
  3195. * Repaint all components
  3196. */
  3197. Controller.prototype.repaint = function repaint() {
  3198. var changed = false;
  3199. // cancel any running repaint request
  3200. if (this.repaintTimer) {
  3201. clearTimeout(this.repaintTimer);
  3202. this.repaintTimer = undefined;
  3203. }
  3204. var done = {};
  3205. function repaint(component, id) {
  3206. if (!(id in done)) {
  3207. // first repaint the components on which this component is dependent
  3208. if (component.depends) {
  3209. component.depends.forEach(function (dep) {
  3210. repaint(dep, dep.id);
  3211. });
  3212. }
  3213. if (component.parent) {
  3214. repaint(component.parent, component.parent.id);
  3215. }
  3216. // repaint the component itself and mark as done
  3217. changed = component.repaint() || changed;
  3218. done[id] = true;
  3219. }
  3220. }
  3221. util.forEach(this.components, repaint);
  3222. // immediately reflow when needed
  3223. if (changed) {
  3224. this.reflow();
  3225. }
  3226. // TODO: limit the number of nested reflows/repaints, prevent loop
  3227. };
  3228. /**
  3229. * Reflow all components
  3230. */
  3231. Controller.prototype.reflow = function reflow() {
  3232. var resized = false;
  3233. // cancel any running repaint request
  3234. if (this.reflowTimer) {
  3235. clearTimeout(this.reflowTimer);
  3236. this.reflowTimer = undefined;
  3237. }
  3238. var done = {};
  3239. function reflow(component, id) {
  3240. if (!(id in done)) {
  3241. // first reflow the components on which this component is dependent
  3242. if (component.depends) {
  3243. component.depends.forEach(function (dep) {
  3244. reflow(dep, dep.id);
  3245. });
  3246. }
  3247. if (component.parent) {
  3248. reflow(component.parent, component.parent.id);
  3249. }
  3250. // reflow the component itself and mark as done
  3251. resized = component.reflow() || resized;
  3252. done[id] = true;
  3253. }
  3254. }
  3255. util.forEach(this.components, reflow);
  3256. // immediately repaint when needed
  3257. if (resized) {
  3258. this.repaint();
  3259. }
  3260. // TODO: limit the number of nested reflows/repaints, prevent loop
  3261. };
  3262. /**
  3263. * Prototype for visual components
  3264. */
  3265. function Component () {
  3266. this.id = null;
  3267. this.parent = null;
  3268. this.depends = null;
  3269. this.controller = null;
  3270. this.options = null;
  3271. this.frame = null; // main DOM element
  3272. this.top = 0;
  3273. this.left = 0;
  3274. this.width = 0;
  3275. this.height = 0;
  3276. }
  3277. /**
  3278. * Set parameters for the frame. Parameters will be merged in current parameter
  3279. * set.
  3280. * @param {Object} options Available parameters:
  3281. * {String | function} [className]
  3282. * {EventBus} [eventBus]
  3283. * {String | Number | function} [left]
  3284. * {String | Number | function} [top]
  3285. * {String | Number | function} [width]
  3286. * {String | Number | function} [height]
  3287. */
  3288. Component.prototype.setOptions = function setOptions(options) {
  3289. if (options) {
  3290. util.extend(this.options, options);
  3291. if (this.controller) {
  3292. this.requestRepaint();
  3293. this.requestReflow();
  3294. }
  3295. }
  3296. };
  3297. /**
  3298. * Get an option value by name
  3299. * The function will first check this.options object, and else will check
  3300. * this.defaultOptions.
  3301. * @param {String} name
  3302. * @return {*} value
  3303. */
  3304. Component.prototype.getOption = function getOption(name) {
  3305. var value;
  3306. if (this.options) {
  3307. value = this.options[name];
  3308. }
  3309. if (value === undefined && this.defaultOptions) {
  3310. value = this.defaultOptions[name];
  3311. }
  3312. return value;
  3313. };
  3314. /**
  3315. * Get the container element of the component, which can be used by a child to
  3316. * add its own widgets. Not all components do have a container for childs, in
  3317. * that case null is returned.
  3318. * @returns {HTMLElement | null} container
  3319. */
  3320. Component.prototype.getContainer = function getContainer() {
  3321. // should be implemented by the component
  3322. return null;
  3323. };
  3324. /**
  3325. * Get the frame element of the component, the outer HTML DOM element.
  3326. * @returns {HTMLElement | null} frame
  3327. */
  3328. Component.prototype.getFrame = function getFrame() {
  3329. return this.frame;
  3330. };
  3331. /**
  3332. * Repaint the component
  3333. * @return {Boolean} changed
  3334. */
  3335. Component.prototype.repaint = function repaint() {
  3336. // should be implemented by the component
  3337. return false;
  3338. };
  3339. /**
  3340. * Reflow the component
  3341. * @return {Boolean} resized
  3342. */
  3343. Component.prototype.reflow = function reflow() {
  3344. // should be implemented by the component
  3345. return false;
  3346. };
  3347. /**
  3348. * Hide the component from the DOM
  3349. * @return {Boolean} changed
  3350. */
  3351. Component.prototype.hide = function hide() {
  3352. if (this.frame && this.frame.parentNode) {
  3353. this.frame.parentNode.removeChild(this.frame);
  3354. return true;
  3355. }
  3356. else {
  3357. return false;
  3358. }
  3359. };
  3360. /**
  3361. * Show the component in the DOM (when not already visible).
  3362. * A repaint will be executed when the component is not visible
  3363. * @return {Boolean} changed
  3364. */
  3365. Component.prototype.show = function show() {
  3366. if (!this.frame || !this.frame.parentNode) {
  3367. return this.repaint();
  3368. }
  3369. else {
  3370. return false;
  3371. }
  3372. };
  3373. /**
  3374. * Request a repaint. The controller will schedule a repaint
  3375. */
  3376. Component.prototype.requestRepaint = function requestRepaint() {
  3377. if (this.controller) {
  3378. this.controller.requestRepaint();
  3379. }
  3380. else {
  3381. throw new Error('Cannot request a repaint: no controller configured');
  3382. // TODO: just do a repaint when no parent is configured?
  3383. }
  3384. };
  3385. /**
  3386. * Request a reflow. The controller will schedule a reflow
  3387. */
  3388. Component.prototype.requestReflow = function requestReflow() {
  3389. if (this.controller) {
  3390. this.controller.requestReflow();
  3391. }
  3392. else {
  3393. throw new Error('Cannot request a reflow: no controller configured');
  3394. // TODO: just do a reflow when no parent is configured?
  3395. }
  3396. };
  3397. /**
  3398. * A panel can contain components
  3399. * @param {Component} [parent]
  3400. * @param {Component[]} [depends] Components on which this components depends
  3401. * (except for the parent)
  3402. * @param {Object} [options] Available parameters:
  3403. * {String | Number | function} [left]
  3404. * {String | Number | function} [top]
  3405. * {String | Number | function} [width]
  3406. * {String | Number | function} [height]
  3407. * {String | function} [className]
  3408. * @constructor Panel
  3409. * @extends Component
  3410. */
  3411. function Panel(parent, depends, options) {
  3412. this.id = util.randomUUID();
  3413. this.parent = parent;
  3414. this.depends = depends;
  3415. this.options = options || {};
  3416. }
  3417. Panel.prototype = new Component();
  3418. /**
  3419. * Set options. Will extend the current options.
  3420. * @param {Object} [options] Available parameters:
  3421. * {String | function} [className]
  3422. * {String | Number | function} [left]
  3423. * {String | Number | function} [top]
  3424. * {String | Number | function} [width]
  3425. * {String | Number | function} [height]
  3426. */
  3427. Panel.prototype.setOptions = Component.prototype.setOptions;
  3428. /**
  3429. * Get the container element of the panel, which can be used by a child to
  3430. * add its own widgets.
  3431. * @returns {HTMLElement} container
  3432. */
  3433. Panel.prototype.getContainer = function () {
  3434. return this.frame;
  3435. };
  3436. /**
  3437. * Repaint the component
  3438. * @return {Boolean} changed
  3439. */
  3440. Panel.prototype.repaint = function () {
  3441. var changed = 0,
  3442. update = util.updateProperty,
  3443. asSize = util.option.asSize,
  3444. options = this.options,
  3445. frame = this.frame;
  3446. if (!frame) {
  3447. frame = document.createElement('div');
  3448. frame.className = 'panel';
  3449. var className = options.className;
  3450. if (className) {
  3451. if (typeof className == 'function') {
  3452. util.addClassName(frame, String(className()));
  3453. }
  3454. else {
  3455. util.addClassName(frame, String(className));
  3456. }
  3457. }
  3458. this.frame = frame;
  3459. changed += 1;
  3460. }
  3461. if (!frame.parentNode) {
  3462. if (!this.parent) {
  3463. throw new Error('Cannot repaint panel: no parent attached');
  3464. }
  3465. var parentContainer = this.parent.getContainer();
  3466. if (!parentContainer) {
  3467. throw new Error('Cannot repaint panel: parent has no container element');
  3468. }
  3469. parentContainer.appendChild(frame);
  3470. changed += 1;
  3471. }
  3472. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3473. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3474. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3475. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3476. return (changed > 0);
  3477. };
  3478. /**
  3479. * Reflow the component
  3480. * @return {Boolean} resized
  3481. */
  3482. Panel.prototype.reflow = function () {
  3483. var changed = 0,
  3484. update = util.updateProperty,
  3485. frame = this.frame;
  3486. if (frame) {
  3487. changed += update(this, 'top', frame.offsetTop);
  3488. changed += update(this, 'left', frame.offsetLeft);
  3489. changed += update(this, 'width', frame.offsetWidth);
  3490. changed += update(this, 'height', frame.offsetHeight);
  3491. }
  3492. else {
  3493. changed += 1;
  3494. }
  3495. return (changed > 0);
  3496. };
  3497. /**
  3498. * A root panel can hold components. The root panel must be initialized with
  3499. * a DOM element as container.
  3500. * @param {HTMLElement} container
  3501. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3502. * @constructor RootPanel
  3503. * @extends Panel
  3504. */
  3505. function RootPanel(container, options) {
  3506. this.id = util.randomUUID();
  3507. this.container = container;
  3508. this.options = options || {};
  3509. this.defaultOptions = {
  3510. autoResize: true
  3511. };
  3512. this.listeners = {}; // event listeners
  3513. }
  3514. RootPanel.prototype = new Panel();
  3515. /**
  3516. * Set options. Will extend the current options.
  3517. * @param {Object} [options] Available parameters:
  3518. * {String | function} [className]
  3519. * {String | Number | function} [left]
  3520. * {String | Number | function} [top]
  3521. * {String | Number | function} [width]
  3522. * {String | Number | function} [height]
  3523. * {Boolean | function} [autoResize]
  3524. */
  3525. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3526. /**
  3527. * Repaint the component
  3528. * @return {Boolean} changed
  3529. */
  3530. RootPanel.prototype.repaint = function () {
  3531. var changed = 0,
  3532. update = util.updateProperty,
  3533. asSize = util.option.asSize,
  3534. options = this.options,
  3535. frame = this.frame;
  3536. if (!frame) {
  3537. frame = document.createElement('div');
  3538. frame.className = 'graph panel';
  3539. var className = options.className;
  3540. if (className) {
  3541. util.addClassName(frame, util.option.asString(className));
  3542. }
  3543. this.frame = frame;
  3544. changed += 1;
  3545. }
  3546. if (!frame.parentNode) {
  3547. if (!this.container) {
  3548. throw new Error('Cannot repaint root panel: no container attached');
  3549. }
  3550. this.container.appendChild(frame);
  3551. changed += 1;
  3552. }
  3553. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3554. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3555. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3556. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3557. this._updateEventEmitters();
  3558. this._updateWatch();
  3559. return (changed > 0);
  3560. };
  3561. /**
  3562. * Reflow the component
  3563. * @return {Boolean} resized
  3564. */
  3565. RootPanel.prototype.reflow = function () {
  3566. var changed = 0,
  3567. update = util.updateProperty,
  3568. frame = this.frame;
  3569. if (frame) {
  3570. changed += update(this, 'top', frame.offsetTop);
  3571. changed += update(this, 'left', frame.offsetLeft);
  3572. changed += update(this, 'width', frame.offsetWidth);
  3573. changed += update(this, 'height', frame.offsetHeight);
  3574. }
  3575. else {
  3576. changed += 1;
  3577. }
  3578. return (changed > 0);
  3579. };
  3580. /**
  3581. * Update watching for resize, depending on the current option
  3582. * @private
  3583. */
  3584. RootPanel.prototype._updateWatch = function () {
  3585. var autoResize = this.getOption('autoResize');
  3586. if (autoResize) {
  3587. this._watch();
  3588. }
  3589. else {
  3590. this._unwatch();
  3591. }
  3592. };
  3593. /**
  3594. * Watch for changes in the size of the frame. On resize, the Panel will
  3595. * automatically redraw itself.
  3596. * @private
  3597. */
  3598. RootPanel.prototype._watch = function () {
  3599. var me = this;
  3600. this._unwatch();
  3601. var checkSize = function () {
  3602. var autoResize = me.getOption('autoResize');
  3603. if (!autoResize) {
  3604. // stop watching when the option autoResize is changed to false
  3605. me._unwatch();
  3606. return;
  3607. }
  3608. if (me.frame) {
  3609. // check whether the frame is resized
  3610. if ((me.frame.clientWidth != me.width) ||
  3611. (me.frame.clientHeight != me.height)) {
  3612. me.requestReflow();
  3613. }
  3614. }
  3615. };
  3616. // TODO: automatically cleanup the event listener when the frame is deleted
  3617. util.addEventListener(window, 'resize', checkSize);
  3618. this.watchTimer = setInterval(checkSize, 1000);
  3619. };
  3620. /**
  3621. * Stop watching for a resize of the frame.
  3622. * @private
  3623. */
  3624. RootPanel.prototype._unwatch = function () {
  3625. if (this.watchTimer) {
  3626. clearInterval(this.watchTimer);
  3627. this.watchTimer = undefined;
  3628. }
  3629. // TODO: remove event listener on window.resize
  3630. };
  3631. /**
  3632. * Event handler
  3633. * @param {String} event name of the event, for example 'click', 'mousemove'
  3634. * @param {function} callback callback handler, invoked with the raw HTML Event
  3635. * as parameter.
  3636. */
  3637. RootPanel.prototype.on = function (event, callback) {
  3638. // register the listener at this component
  3639. var arr = this.listeners[event];
  3640. if (!arr) {
  3641. arr = [];
  3642. this.listeners[event] = arr;
  3643. }
  3644. arr.push(callback);
  3645. this._updateEventEmitters();
  3646. };
  3647. /**
  3648. * Update the event listeners for all event emitters
  3649. * @private
  3650. */
  3651. RootPanel.prototype._updateEventEmitters = function () {
  3652. if (this.listeners) {
  3653. var me = this;
  3654. util.forEach(this.listeners, function (listeners, event) {
  3655. if (!me.emitters) {
  3656. me.emitters = {};
  3657. }
  3658. if (!(event in me.emitters)) {
  3659. // create event
  3660. var frame = me.frame;
  3661. if (frame) {
  3662. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3663. var callback = function(event) {
  3664. listeners.forEach(function (listener) {
  3665. // TODO: filter on event target!
  3666. listener(event);
  3667. });
  3668. };
  3669. me.emitters[event] = callback;
  3670. util.addEventListener(frame, event, callback);
  3671. }
  3672. }
  3673. });
  3674. // TODO: be able to delete event listeners
  3675. // TODO: be able to move event listeners to a parent when available
  3676. }
  3677. };
  3678. /**
  3679. * A horizontal time axis
  3680. * @param {Component} parent
  3681. * @param {Component[]} [depends] Components on which this components depends
  3682. * (except for the parent)
  3683. * @param {Object} [options] See TimeAxis.setOptions for the available
  3684. * options.
  3685. * @constructor TimeAxis
  3686. * @extends Component
  3687. */
  3688. function TimeAxis (parent, depends, options) {
  3689. this.id = util.randomUUID();
  3690. this.parent = parent;
  3691. this.depends = depends;
  3692. this.dom = {
  3693. majorLines: [],
  3694. majorTexts: [],
  3695. minorLines: [],
  3696. minorTexts: [],
  3697. redundant: {
  3698. majorLines: [],
  3699. majorTexts: [],
  3700. minorLines: [],
  3701. minorTexts: []
  3702. }
  3703. };
  3704. this.props = {
  3705. range: {
  3706. start: 0,
  3707. end: 0,
  3708. minimumStep: 0
  3709. },
  3710. lineTop: 0
  3711. };
  3712. this.options = options || {};
  3713. this.defaultOptions = {
  3714. orientation: 'bottom', // supported: 'top', 'bottom'
  3715. // TODO: implement timeaxis orientations 'left' and 'right'
  3716. showMinorLabels: true,
  3717. showMajorLabels: true
  3718. };
  3719. this.conversion = null;
  3720. this.range = null;
  3721. }
  3722. TimeAxis.prototype = new Component();
  3723. // TODO: comment options
  3724. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3725. /**
  3726. * Set a range (start and end)
  3727. * @param {Range | Object} range A Range or an object containing start and end.
  3728. */
  3729. TimeAxis.prototype.setRange = function (range) {
  3730. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3731. throw new TypeError('Range must be an instance of Range, ' +
  3732. 'or an object containing start and end.');
  3733. }
  3734. this.range = range;
  3735. };
  3736. /**
  3737. * Convert a position on screen (pixels) to a datetime
  3738. * @param {int} x Position on the screen in pixels
  3739. * @return {Date} time The datetime the corresponds with given position x
  3740. */
  3741. TimeAxis.prototype.toTime = function(x) {
  3742. var conversion = this.conversion;
  3743. return new Date(x / conversion.factor + conversion.offset);
  3744. };
  3745. /**
  3746. * Convert a datetime (Date object) into a position on the screen
  3747. * @param {Date} time A date
  3748. * @return {int} x The position on the screen in pixels which corresponds
  3749. * with the given date.
  3750. * @private
  3751. */
  3752. TimeAxis.prototype.toScreen = function(time) {
  3753. var conversion = this.conversion;
  3754. return (time.valueOf() - conversion.offset) * conversion.factor;
  3755. };
  3756. /**
  3757. * Repaint the component
  3758. * @return {Boolean} changed
  3759. */
  3760. TimeAxis.prototype.repaint = function () {
  3761. var changed = 0,
  3762. update = util.updateProperty,
  3763. asSize = util.option.asSize,
  3764. options = this.options,
  3765. orientation = this.getOption('orientation'),
  3766. props = this.props,
  3767. step = this.step;
  3768. var frame = this.frame;
  3769. if (!frame) {
  3770. frame = document.createElement('div');
  3771. this.frame = frame;
  3772. changed += 1;
  3773. }
  3774. frame.className = 'axis ' + orientation;
  3775. // TODO: custom className?
  3776. if (!frame.parentNode) {
  3777. if (!this.parent) {
  3778. throw new Error('Cannot repaint time axis: no parent attached');
  3779. }
  3780. var parentContainer = this.parent.getContainer();
  3781. if (!parentContainer) {
  3782. throw new Error('Cannot repaint time axis: parent has no container element');
  3783. }
  3784. parentContainer.appendChild(frame);
  3785. changed += 1;
  3786. }
  3787. var parent = frame.parentNode;
  3788. if (parent) {
  3789. var beforeChild = frame.nextSibling;
  3790. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3791. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3792. (this.props.parentHeight - this.height) + 'px' :
  3793. '0px';
  3794. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3795. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3796. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3797. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3798. // get characters width and height
  3799. this._repaintMeasureChars();
  3800. if (this.step) {
  3801. this._repaintStart();
  3802. step.first();
  3803. var xFirstMajorLabel = undefined;
  3804. var max = 0;
  3805. while (step.hasNext() && max < 1000) {
  3806. max++;
  3807. var cur = step.getCurrent(),
  3808. x = this.toScreen(cur),
  3809. isMajor = step.isMajor();
  3810. // TODO: lines must have a width, such that we can create css backgrounds
  3811. if (this.getOption('showMinorLabels')) {
  3812. this._repaintMinorText(x, step.getLabelMinor());
  3813. }
  3814. if (isMajor && this.getOption('showMajorLabels')) {
  3815. if (x > 0) {
  3816. if (xFirstMajorLabel == undefined) {
  3817. xFirstMajorLabel = x;
  3818. }
  3819. this._repaintMajorText(x, step.getLabelMajor());
  3820. }
  3821. this._repaintMajorLine(x);
  3822. }
  3823. else {
  3824. this._repaintMinorLine(x);
  3825. }
  3826. step.next();
  3827. }
  3828. // create a major label on the left when needed
  3829. if (this.getOption('showMajorLabels')) {
  3830. var leftTime = this.toTime(0),
  3831. leftText = step.getLabelMajor(leftTime),
  3832. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3833. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3834. this._repaintMajorText(0, leftText);
  3835. }
  3836. }
  3837. this._repaintEnd();
  3838. }
  3839. this._repaintLine();
  3840. // put frame online again
  3841. if (beforeChild) {
  3842. parent.insertBefore(frame, beforeChild);
  3843. }
  3844. else {
  3845. parent.appendChild(frame)
  3846. }
  3847. }
  3848. return (changed > 0);
  3849. };
  3850. /**
  3851. * Start a repaint. Move all DOM elements to a redundant list, where they
  3852. * can be picked for re-use, or can be cleaned up in the end
  3853. * @private
  3854. */
  3855. TimeAxis.prototype._repaintStart = function () {
  3856. var dom = this.dom,
  3857. redundant = dom.redundant;
  3858. redundant.majorLines = dom.majorLines;
  3859. redundant.majorTexts = dom.majorTexts;
  3860. redundant.minorLines = dom.minorLines;
  3861. redundant.minorTexts = dom.minorTexts;
  3862. dom.majorLines = [];
  3863. dom.majorTexts = [];
  3864. dom.minorLines = [];
  3865. dom.minorTexts = [];
  3866. };
  3867. /**
  3868. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3869. * @private
  3870. */
  3871. TimeAxis.prototype._repaintEnd = function () {
  3872. util.forEach(this.dom.redundant, function (arr) {
  3873. while (arr.length) {
  3874. var elem = arr.pop();
  3875. if (elem && elem.parentNode) {
  3876. elem.parentNode.removeChild(elem);
  3877. }
  3878. }
  3879. });
  3880. };
  3881. /**
  3882. * Create a minor label for the axis at position x
  3883. * @param {Number} x
  3884. * @param {String} text
  3885. * @private
  3886. */
  3887. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3888. // reuse redundant label
  3889. var label = this.dom.redundant.minorTexts.shift();
  3890. if (!label) {
  3891. // create new label
  3892. var content = document.createTextNode('');
  3893. label = document.createElement('div');
  3894. label.appendChild(content);
  3895. label.className = 'text minor';
  3896. this.frame.appendChild(label);
  3897. }
  3898. this.dom.minorTexts.push(label);
  3899. label.childNodes[0].nodeValue = text;
  3900. label.style.left = x + 'px';
  3901. label.style.top = this.props.minorLabelTop + 'px';
  3902. //label.title = title; // TODO: this is a heavy operation
  3903. };
  3904. /**
  3905. * Create a Major label for the axis at position x
  3906. * @param {Number} x
  3907. * @param {String} text
  3908. * @private
  3909. */
  3910. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3911. // reuse redundant label
  3912. var label = this.dom.redundant.majorTexts.shift();
  3913. if (!label) {
  3914. // create label
  3915. var content = document.createTextNode(text);
  3916. label = document.createElement('div');
  3917. label.className = 'text major';
  3918. label.appendChild(content);
  3919. this.frame.appendChild(label);
  3920. }
  3921. this.dom.majorTexts.push(label);
  3922. label.childNodes[0].nodeValue = text;
  3923. label.style.top = this.props.majorLabelTop + 'px';
  3924. label.style.left = x + 'px';
  3925. //label.title = title; // TODO: this is a heavy operation
  3926. };
  3927. /**
  3928. * Create a minor line for the axis at position x
  3929. * @param {Number} x
  3930. * @private
  3931. */
  3932. TimeAxis.prototype._repaintMinorLine = function (x) {
  3933. // reuse redundant line
  3934. var line = this.dom.redundant.minorLines.shift();
  3935. if (!line) {
  3936. // create vertical line
  3937. line = document.createElement('div');
  3938. line.className = 'grid vertical minor';
  3939. this.frame.appendChild(line);
  3940. }
  3941. this.dom.minorLines.push(line);
  3942. var props = this.props;
  3943. line.style.top = props.minorLineTop + 'px';
  3944. line.style.height = props.minorLineHeight + 'px';
  3945. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3946. };
  3947. /**
  3948. * Create a Major line for the axis at position x
  3949. * @param {Number} x
  3950. * @private
  3951. */
  3952. TimeAxis.prototype._repaintMajorLine = function (x) {
  3953. // reuse redundant line
  3954. var line = this.dom.redundant.majorLines.shift();
  3955. if (!line) {
  3956. // create vertical line
  3957. line = document.createElement('DIV');
  3958. line.className = 'grid vertical major';
  3959. this.frame.appendChild(line);
  3960. }
  3961. this.dom.majorLines.push(line);
  3962. var props = this.props;
  3963. line.style.top = props.majorLineTop + 'px';
  3964. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3965. line.style.height = props.majorLineHeight + 'px';
  3966. };
  3967. /**
  3968. * Repaint the horizontal line for the axis
  3969. * @private
  3970. */
  3971. TimeAxis.prototype._repaintLine = function() {
  3972. var line = this.dom.line,
  3973. frame = this.frame,
  3974. options = this.options;
  3975. // line before all axis elements
  3976. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  3977. if (line) {
  3978. // put this line at the end of all childs
  3979. frame.removeChild(line);
  3980. frame.appendChild(line);
  3981. }
  3982. else {
  3983. // create the axis line
  3984. line = document.createElement('div');
  3985. line.className = 'grid horizontal major';
  3986. frame.appendChild(line);
  3987. this.dom.line = line;
  3988. }
  3989. line.style.top = this.props.lineTop + 'px';
  3990. }
  3991. else {
  3992. if (line && axis.parentElement) {
  3993. frame.removeChild(axis.line);
  3994. delete this.dom.line;
  3995. }
  3996. }
  3997. };
  3998. /**
  3999. * Create characters used to determine the size of text on the axis
  4000. * @private
  4001. */
  4002. TimeAxis.prototype._repaintMeasureChars = function () {
  4003. // calculate the width and height of a single character
  4004. // this is used to calculate the step size, and also the positioning of the
  4005. // axis
  4006. var dom = this.dom,
  4007. text;
  4008. if (!dom.measureCharMinor) {
  4009. text = document.createTextNode('0');
  4010. var measureCharMinor = document.createElement('DIV');
  4011. measureCharMinor.className = 'text minor measure';
  4012. measureCharMinor.appendChild(text);
  4013. this.frame.appendChild(measureCharMinor);
  4014. dom.measureCharMinor = measureCharMinor;
  4015. }
  4016. if (!dom.measureCharMajor) {
  4017. text = document.createTextNode('0');
  4018. var measureCharMajor = document.createElement('DIV');
  4019. measureCharMajor.className = 'text major measure';
  4020. measureCharMajor.appendChild(text);
  4021. this.frame.appendChild(measureCharMajor);
  4022. dom.measureCharMajor = measureCharMajor;
  4023. }
  4024. };
  4025. /**
  4026. * Reflow the component
  4027. * @return {Boolean} resized
  4028. */
  4029. TimeAxis.prototype.reflow = function () {
  4030. var changed = 0,
  4031. update = util.updateProperty,
  4032. frame = this.frame,
  4033. range = this.range;
  4034. if (!range) {
  4035. throw new Error('Cannot repaint time axis: no range configured');
  4036. }
  4037. if (frame) {
  4038. changed += update(this, 'top', frame.offsetTop);
  4039. changed += update(this, 'left', frame.offsetLeft);
  4040. // calculate size of a character
  4041. var props = this.props,
  4042. showMinorLabels = this.getOption('showMinorLabels'),
  4043. showMajorLabels = this.getOption('showMajorLabels'),
  4044. measureCharMinor = this.dom.measureCharMinor,
  4045. measureCharMajor = this.dom.measureCharMajor;
  4046. if (measureCharMinor) {
  4047. props.minorCharHeight = measureCharMinor.clientHeight;
  4048. props.minorCharWidth = measureCharMinor.clientWidth;
  4049. }
  4050. if (measureCharMajor) {
  4051. props.majorCharHeight = measureCharMajor.clientHeight;
  4052. props.majorCharWidth = measureCharMajor.clientWidth;
  4053. }
  4054. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4055. if (parentHeight != props.parentHeight) {
  4056. props.parentHeight = parentHeight;
  4057. changed += 1;
  4058. }
  4059. switch (this.getOption('orientation')) {
  4060. case 'bottom':
  4061. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4062. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4063. props.minorLabelTop = 0;
  4064. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4065. props.minorLineTop = -this.top;
  4066. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4067. props.minorLineWidth = 1; // TODO: really calculate width
  4068. props.majorLineTop = -this.top;
  4069. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4070. props.majorLineWidth = 1; // TODO: really calculate width
  4071. props.lineTop = 0;
  4072. break;
  4073. case 'top':
  4074. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4075. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4076. props.majorLabelTop = 0;
  4077. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4078. props.minorLineTop = props.minorLabelTop;
  4079. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4080. props.minorLineWidth = 1; // TODO: really calculate width
  4081. props.majorLineTop = 0;
  4082. props.majorLineHeight = Math.max(parentHeight - this.top);
  4083. props.majorLineWidth = 1; // TODO: really calculate width
  4084. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4085. break;
  4086. default:
  4087. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4088. }
  4089. var height = props.minorLabelHeight + props.majorLabelHeight;
  4090. changed += update(this, 'width', frame.offsetWidth);
  4091. changed += update(this, 'height', height);
  4092. // calculate range and step
  4093. this._updateConversion();
  4094. var start = util.cast(range.start, 'Date'),
  4095. end = util.cast(range.end, 'Date'),
  4096. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  4097. this.step = new TimeStep(start, end, minimumStep);
  4098. changed += update(props.range, 'start', start.valueOf());
  4099. changed += update(props.range, 'end', end.valueOf());
  4100. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4101. }
  4102. return (changed > 0);
  4103. };
  4104. /**
  4105. * Calculate the factor and offset to convert a position on screen to the
  4106. * corresponding date and vice versa.
  4107. * After the method _updateConversion is executed once, the methods toTime
  4108. * and toScreen can be used.
  4109. * @private
  4110. */
  4111. TimeAxis.prototype._updateConversion = function() {
  4112. var range = this.range;
  4113. if (!range) {
  4114. throw new Error('No range configured');
  4115. }
  4116. if (range.conversion) {
  4117. this.conversion = range.conversion(this.width);
  4118. }
  4119. else {
  4120. this.conversion = Range.conversion(range.start, range.end, this.width);
  4121. }
  4122. };
  4123. /**
  4124. * An ItemSet holds a set of items and ranges which can be displayed in a
  4125. * range. The width is determined by the parent of the ItemSet, and the height
  4126. * is determined by the size of the items.
  4127. * @param {Component} parent
  4128. * @param {Component[]} [depends] Components on which this components depends
  4129. * (except for the parent)
  4130. * @param {Object} [options] See ItemSet.setOptions for the available
  4131. * options.
  4132. * @constructor ItemSet
  4133. * @extends Panel
  4134. */
  4135. // TODO: improve performance by replacing all Array.forEach with a for loop
  4136. function ItemSet(parent, depends, options) {
  4137. this.id = util.randomUUID();
  4138. this.parent = parent;
  4139. this.depends = depends;
  4140. // one options object is shared by this itemset and all its items
  4141. this.options = options || {};
  4142. this.defaultOptions = {
  4143. type: 'box',
  4144. align: 'center',
  4145. orientation: 'bottom',
  4146. margin: {
  4147. axis: 20,
  4148. item: 10
  4149. },
  4150. padding: 5
  4151. };
  4152. this.dom = {};
  4153. var me = this;
  4154. this.itemsData = null; // DataSet
  4155. this.range = null; // Range or Object {start: number, end: number}
  4156. this.listeners = {
  4157. 'add': function (event, params, senderId) {
  4158. if (senderId != me.id) {
  4159. me._onAdd(params.items);
  4160. }
  4161. },
  4162. 'update': function (event, params, senderId) {
  4163. if (senderId != me.id) {
  4164. me._onUpdate(params.items);
  4165. }
  4166. },
  4167. 'remove': function (event, params, senderId) {
  4168. if (senderId != me.id) {
  4169. me._onRemove(params.items);
  4170. }
  4171. }
  4172. };
  4173. this.items = {}; // object with an Item for every data item
  4174. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4175. this.stack = new Stack(this, Object.create(this.options));
  4176. this.conversion = null;
  4177. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4178. }
  4179. ItemSet.prototype = new Panel();
  4180. // available item types will be registered here
  4181. ItemSet.types = {
  4182. box: ItemBox,
  4183. range: ItemRange,
  4184. point: ItemPoint
  4185. };
  4186. /**
  4187. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4188. * @param {Object} [options] The following options are available:
  4189. * {String | function} [className]
  4190. * class name for the itemset
  4191. * {String} [type]
  4192. * Default type for the items. Choose from 'box'
  4193. * (default), 'point', or 'range'. The default
  4194. * Style can be overwritten by individual items.
  4195. * {String} align
  4196. * Alignment for the items, only applicable for
  4197. * ItemBox. Choose 'center' (default), 'left', or
  4198. * 'right'.
  4199. * {String} orientation
  4200. * Orientation of the item set. Choose 'top' or
  4201. * 'bottom' (default).
  4202. * {Number} margin.axis
  4203. * Margin between the axis and the items in pixels.
  4204. * Default is 20.
  4205. * {Number} margin.item
  4206. * Margin between items in pixels. Default is 10.
  4207. * {Number} padding
  4208. * Padding of the contents of an item in pixels.
  4209. * Must correspond with the items css. Default is 5.
  4210. */
  4211. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4212. /**
  4213. * Set range (start and end).
  4214. * @param {Range | Object} range A Range or an object containing start and end.
  4215. */
  4216. ItemSet.prototype.setRange = function setRange(range) {
  4217. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4218. throw new TypeError('Range must be an instance of Range, ' +
  4219. 'or an object containing start and end.');
  4220. }
  4221. this.range = range;
  4222. };
  4223. /**
  4224. * Repaint the component
  4225. * @return {Boolean} changed
  4226. */
  4227. ItemSet.prototype.repaint = function repaint() {
  4228. var changed = 0,
  4229. update = util.updateProperty,
  4230. asSize = util.option.asSize,
  4231. options = this.options,
  4232. orientation = this.getOption('orientation'),
  4233. defaultOptions = this.defaultOptions,
  4234. frame = this.frame;
  4235. if (!frame) {
  4236. frame = document.createElement('div');
  4237. frame.className = 'itemset';
  4238. var className = options.className;
  4239. if (className) {
  4240. util.addClassName(frame, util.option.asString(className));
  4241. }
  4242. // create background panel
  4243. var background = document.createElement('div');
  4244. background.className = 'background';
  4245. frame.appendChild(background);
  4246. this.dom.background = background;
  4247. // create foreground panel
  4248. var foreground = document.createElement('div');
  4249. foreground.className = 'foreground';
  4250. frame.appendChild(foreground);
  4251. this.dom.foreground = foreground;
  4252. // create axis panel
  4253. var axis = document.createElement('div');
  4254. axis.className = 'itemset-axis';
  4255. //frame.appendChild(axis);
  4256. this.dom.axis = axis;
  4257. this.frame = frame;
  4258. changed += 1;
  4259. }
  4260. if (!this.parent) {
  4261. throw new Error('Cannot repaint itemset: no parent attached');
  4262. }
  4263. var parentContainer = this.parent.getContainer();
  4264. if (!parentContainer) {
  4265. throw new Error('Cannot repaint itemset: parent has no container element');
  4266. }
  4267. if (!frame.parentNode) {
  4268. parentContainer.appendChild(frame);
  4269. changed += 1;
  4270. }
  4271. if (!this.dom.axis.parentNode) {
  4272. parentContainer.appendChild(this.dom.axis);
  4273. changed += 1;
  4274. }
  4275. // reposition frame
  4276. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4277. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4278. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4279. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4280. // reposition axis
  4281. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4282. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4283. if (orientation == 'bottom') {
  4284. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4285. }
  4286. else { // orientation == 'top'
  4287. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4288. }
  4289. this._updateConversion();
  4290. var me = this,
  4291. queue = this.queue,
  4292. itemsData = this.itemsData,
  4293. items = this.items,
  4294. dataOptions = {
  4295. fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type']
  4296. };
  4297. // TODO: copy options from the itemset itself?
  4298. // show/hide added/changed/removed items
  4299. Object.keys(queue).forEach(function (id) {
  4300. //var entry = queue[id];
  4301. var action = queue[id];
  4302. var item = items[id];
  4303. //var item = entry.item;
  4304. //noinspection FallthroughInSwitchStatementJS
  4305. switch (action) {
  4306. case 'add':
  4307. case 'update':
  4308. var itemData = itemsData && itemsData.get(id, dataOptions);
  4309. if (itemData) {
  4310. var type = itemData.type ||
  4311. (itemData.start && itemData.end && 'range') ||
  4312. options.type ||
  4313. 'box';
  4314. var constructor = ItemSet.types[type];
  4315. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4316. if (item) {
  4317. // update item
  4318. if (!constructor || !(item instanceof constructor)) {
  4319. // item type has changed, hide and delete the item
  4320. changed += item.hide();
  4321. item = null;
  4322. }
  4323. else {
  4324. item.data = itemData; // TODO: create a method item.setData ?
  4325. changed++;
  4326. }
  4327. }
  4328. if (!item) {
  4329. // create item
  4330. if (constructor) {
  4331. item = new constructor(me, itemData, options, defaultOptions);
  4332. changed++;
  4333. }
  4334. else {
  4335. throw new TypeError('Unknown item type "' + type + '"');
  4336. }
  4337. }
  4338. // force a repaint (not only a reposition)
  4339. item.repaint();
  4340. items[id] = item;
  4341. }
  4342. // update queue
  4343. delete queue[id];
  4344. break;
  4345. case 'remove':
  4346. if (item) {
  4347. // remove DOM of the item
  4348. changed += item.hide();
  4349. }
  4350. // update lists
  4351. delete items[id];
  4352. delete queue[id];
  4353. break;
  4354. default:
  4355. console.log('Error: unknown action "' + action + '"');
  4356. }
  4357. });
  4358. // reposition all items. Show items only when in the visible area
  4359. util.forEach(this.items, function (item) {
  4360. if (item.visible) {
  4361. changed += item.show();
  4362. item.reposition();
  4363. }
  4364. else {
  4365. changed += item.hide();
  4366. }
  4367. });
  4368. return (changed > 0);
  4369. };
  4370. /**
  4371. * Get the foreground container element
  4372. * @return {HTMLElement} foreground
  4373. */
  4374. ItemSet.prototype.getForeground = function getForeground() {
  4375. return this.dom.foreground;
  4376. };
  4377. /**
  4378. * Get the background container element
  4379. * @return {HTMLElement} background
  4380. */
  4381. ItemSet.prototype.getBackground = function getBackground() {
  4382. return this.dom.background;
  4383. };
  4384. /**
  4385. * Get the axis container element
  4386. * @return {HTMLElement} axis
  4387. */
  4388. ItemSet.prototype.getAxis = function getAxis() {
  4389. return this.dom.axis;
  4390. };
  4391. /**
  4392. * Reflow the component
  4393. * @return {Boolean} resized
  4394. */
  4395. ItemSet.prototype.reflow = function reflow () {
  4396. var changed = 0,
  4397. options = this.options,
  4398. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4399. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4400. update = util.updateProperty,
  4401. asNumber = util.option.asNumber,
  4402. asSize = util.option.asSize,
  4403. frame = this.frame;
  4404. if (frame) {
  4405. this._updateConversion();
  4406. util.forEach(this.items, function (item) {
  4407. changed += item.reflow();
  4408. });
  4409. // TODO: stack.update should be triggered via an event, in stack itself
  4410. // TODO: only update the stack when there are changed items
  4411. this.stack.update();
  4412. var maxHeight = asNumber(options.maxHeight);
  4413. var fixedHeight = (asSize(options.height) != null);
  4414. var height;
  4415. if (fixedHeight) {
  4416. height = frame.offsetHeight;
  4417. }
  4418. else {
  4419. // height is not specified, determine the height from the height and positioned items
  4420. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4421. if (visibleItems.length) {
  4422. var min = visibleItems[0].top;
  4423. var max = visibleItems[0].top + visibleItems[0].height;
  4424. util.forEach(visibleItems, function (item) {
  4425. min = Math.min(min, item.top);
  4426. max = Math.max(max, (item.top + item.height));
  4427. });
  4428. height = (max - min) + marginAxis + marginItem;
  4429. }
  4430. else {
  4431. height = marginAxis + marginItem;
  4432. }
  4433. }
  4434. if (maxHeight != null) {
  4435. height = Math.min(height, maxHeight);
  4436. }
  4437. changed += update(this, 'height', height);
  4438. // calculate height from items
  4439. changed += update(this, 'top', frame.offsetTop);
  4440. changed += update(this, 'left', frame.offsetLeft);
  4441. changed += update(this, 'width', frame.offsetWidth);
  4442. }
  4443. else {
  4444. changed += 1;
  4445. }
  4446. return (changed > 0);
  4447. };
  4448. /**
  4449. * Hide this component from the DOM
  4450. * @return {Boolean} changed
  4451. */
  4452. ItemSet.prototype.hide = function hide() {
  4453. var changed = false;
  4454. // remove the DOM
  4455. if (this.frame && this.frame.parentNode) {
  4456. this.frame.parentNode.removeChild(this.frame);
  4457. changed = true;
  4458. }
  4459. if (this.dom.axis && this.dom.axis.parentNode) {
  4460. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4461. changed = true;
  4462. }
  4463. return changed;
  4464. };
  4465. /**
  4466. * Set items
  4467. * @param {vis.DataSet | null} items
  4468. */
  4469. ItemSet.prototype.setItems = function setItems(items) {
  4470. var me = this,
  4471. ids,
  4472. oldItemsData = this.itemsData;
  4473. // replace the dataset
  4474. if (!items) {
  4475. this.itemsData = null;
  4476. }
  4477. else if (items instanceof DataSet || items instanceof DataView) {
  4478. this.itemsData = items;
  4479. }
  4480. else {
  4481. throw new TypeError('Data must be an instance of DataSet');
  4482. }
  4483. if (oldItemsData) {
  4484. // unsubscribe from old dataset
  4485. util.forEach(this.listeners, function (callback, event) {
  4486. oldItemsData.unsubscribe(event, callback);
  4487. });
  4488. // remove all drawn items
  4489. ids = oldItemsData.getIds();
  4490. this._onRemove(ids);
  4491. }
  4492. if (this.itemsData) {
  4493. // subscribe to new dataset
  4494. var id = this.id;
  4495. util.forEach(this.listeners, function (callback, event) {
  4496. me.itemsData.subscribe(event, callback, id);
  4497. });
  4498. // draw all new items
  4499. ids = this.itemsData.getIds();
  4500. this._onAdd(ids);
  4501. }
  4502. };
  4503. /**
  4504. * Get the current items items
  4505. * @returns {vis.DataSet | null}
  4506. */
  4507. ItemSet.prototype.getItems = function getItems() {
  4508. return this.itemsData;
  4509. };
  4510. /**
  4511. * Handle updated items
  4512. * @param {Number[]} ids
  4513. * @private
  4514. */
  4515. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4516. this._toQueue('update', ids);
  4517. };
  4518. /**
  4519. * Handle changed items
  4520. * @param {Number[]} ids
  4521. * @private
  4522. */
  4523. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4524. this._toQueue('add', ids);
  4525. };
  4526. /**
  4527. * Handle removed items
  4528. * @param {Number[]} ids
  4529. * @private
  4530. */
  4531. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4532. this._toQueue('remove', ids);
  4533. };
  4534. /**
  4535. * Put items in the queue to be added/updated/remove
  4536. * @param {String} action can be 'add', 'update', 'remove'
  4537. * @param {Number[]} ids
  4538. */
  4539. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  4540. var queue = this.queue;
  4541. ids.forEach(function (id) {
  4542. queue[id] = action;
  4543. });
  4544. if (this.controller) {
  4545. //this.requestReflow();
  4546. this.requestRepaint();
  4547. }
  4548. };
  4549. /**
  4550. * Calculate the factor and offset to convert a position on screen to the
  4551. * corresponding date and vice versa.
  4552. * After the method _updateConversion is executed once, the methods toTime
  4553. * and toScreen can be used.
  4554. * @private
  4555. */
  4556. ItemSet.prototype._updateConversion = function _updateConversion() {
  4557. var range = this.range;
  4558. if (!range) {
  4559. throw new Error('No range configured');
  4560. }
  4561. if (range.conversion) {
  4562. this.conversion = range.conversion(this.width);
  4563. }
  4564. else {
  4565. this.conversion = Range.conversion(range.start, range.end, this.width);
  4566. }
  4567. };
  4568. /**
  4569. * Convert a position on screen (pixels) to a datetime
  4570. * Before this method can be used, the method _updateConversion must be
  4571. * executed once.
  4572. * @param {int} x Position on the screen in pixels
  4573. * @return {Date} time The datetime the corresponds with given position x
  4574. */
  4575. ItemSet.prototype.toTime = function toTime(x) {
  4576. var conversion = this.conversion;
  4577. return new Date(x / conversion.factor + conversion.offset);
  4578. };
  4579. /**
  4580. * Convert a datetime (Date object) into a position on the screen
  4581. * Before this method can be used, the method _updateConversion must be
  4582. * executed once.
  4583. * @param {Date} time A date
  4584. * @return {int} x The position on the screen in pixels which corresponds
  4585. * with the given date.
  4586. */
  4587. ItemSet.prototype.toScreen = function toScreen(time) {
  4588. var conversion = this.conversion;
  4589. return (time.valueOf() - conversion.offset) * conversion.factor;
  4590. };
  4591. /**
  4592. * @constructor Item
  4593. * @param {ItemSet} parent
  4594. * @param {Object} data Object containing (optional) parameters type,
  4595. * start, end, content, group, className.
  4596. * @param {Object} [options] Options to set initial property values
  4597. * @param {Object} [defaultOptions] default options
  4598. * // TODO: describe available options
  4599. */
  4600. function Item (parent, data, options, defaultOptions) {
  4601. this.parent = parent;
  4602. this.data = data;
  4603. this.dom = null;
  4604. this.options = options || {};
  4605. this.defaultOptions = defaultOptions || {};
  4606. this.selected = false;
  4607. this.visible = false;
  4608. this.top = 0;
  4609. this.left = 0;
  4610. this.width = 0;
  4611. this.height = 0;
  4612. }
  4613. /**
  4614. * Select current item
  4615. */
  4616. Item.prototype.select = function select() {
  4617. this.selected = true;
  4618. };
  4619. /**
  4620. * Unselect current item
  4621. */
  4622. Item.prototype.unselect = function unselect() {
  4623. this.selected = false;
  4624. };
  4625. /**
  4626. * Show the Item in the DOM (when not already visible)
  4627. * @return {Boolean} changed
  4628. */
  4629. Item.prototype.show = function show() {
  4630. return false;
  4631. };
  4632. /**
  4633. * Hide the Item from the DOM (when visible)
  4634. * @return {Boolean} changed
  4635. */
  4636. Item.prototype.hide = function hide() {
  4637. return false;
  4638. };
  4639. /**
  4640. * Repaint the item
  4641. * @return {Boolean} changed
  4642. */
  4643. Item.prototype.repaint = function repaint() {
  4644. // should be implemented by the item
  4645. return false;
  4646. };
  4647. /**
  4648. * Reflow the item
  4649. * @return {Boolean} resized
  4650. */
  4651. Item.prototype.reflow = function reflow() {
  4652. // should be implemented by the item
  4653. return false;
  4654. };
  4655. /**
  4656. * @constructor ItemBox
  4657. * @extends Item
  4658. * @param {ItemSet} parent
  4659. * @param {Object} data Object containing parameters start
  4660. * content, className.
  4661. * @param {Object} [options] Options to set initial property values
  4662. * @param {Object} [defaultOptions] default options
  4663. * // TODO: describe available options
  4664. */
  4665. function ItemBox (parent, data, options, defaultOptions) {
  4666. this.props = {
  4667. dot: {
  4668. left: 0,
  4669. top: 0,
  4670. width: 0,
  4671. height: 0
  4672. },
  4673. line: {
  4674. top: 0,
  4675. left: 0,
  4676. width: 0,
  4677. height: 0
  4678. }
  4679. };
  4680. Item.call(this, parent, data, options, defaultOptions);
  4681. }
  4682. ItemBox.prototype = new Item (null, null);
  4683. /**
  4684. * Select the item
  4685. * @override
  4686. */
  4687. ItemBox.prototype.select = function select() {
  4688. this.selected = true;
  4689. // TODO: select and unselect
  4690. };
  4691. /**
  4692. * Unselect the item
  4693. * @override
  4694. */
  4695. ItemBox.prototype.unselect = function unselect() {
  4696. this.selected = false;
  4697. // TODO: select and unselect
  4698. };
  4699. /**
  4700. * Repaint the item
  4701. * @return {Boolean} changed
  4702. */
  4703. ItemBox.prototype.repaint = function repaint() {
  4704. // TODO: make an efficient repaint
  4705. var changed = false;
  4706. var dom = this.dom;
  4707. if (!dom) {
  4708. this._create();
  4709. dom = this.dom;
  4710. changed = true;
  4711. }
  4712. if (dom) {
  4713. if (!this.parent) {
  4714. throw new Error('Cannot repaint item: no parent attached');
  4715. }
  4716. var foreground = this.parent.getForeground();
  4717. if (!foreground) {
  4718. throw new Error('Cannot repaint time axis: ' +
  4719. 'parent has no foreground container element');
  4720. }
  4721. var background = this.parent.getBackground();
  4722. if (!background) {
  4723. throw new Error('Cannot repaint time axis: ' +
  4724. 'parent has no background container element');
  4725. }
  4726. var axis = this.parent.getAxis();
  4727. if (!background) {
  4728. throw new Error('Cannot repaint time axis: ' +
  4729. 'parent has no axis container element');
  4730. }
  4731. if (!dom.box.parentNode) {
  4732. foreground.appendChild(dom.box);
  4733. changed = true;
  4734. }
  4735. if (!dom.line.parentNode) {
  4736. background.appendChild(dom.line);
  4737. changed = true;
  4738. }
  4739. if (!dom.dot.parentNode) {
  4740. axis.appendChild(dom.dot);
  4741. changed = true;
  4742. }
  4743. // update contents
  4744. if (this.data.content != this.content) {
  4745. this.content = this.data.content;
  4746. if (this.content instanceof Element) {
  4747. dom.content.innerHTML = '';
  4748. dom.content.appendChild(this.content);
  4749. }
  4750. else if (this.data.content != undefined) {
  4751. dom.content.innerHTML = this.content;
  4752. }
  4753. else {
  4754. throw new Error('Property "content" missing in item ' + this.data.id);
  4755. }
  4756. changed = true;
  4757. }
  4758. // update class
  4759. var className = (this.data.className? ' ' + this.data.className : '') +
  4760. (this.selected ? ' selected' : '');
  4761. if (this.className != className) {
  4762. this.className = className;
  4763. dom.box.className = 'item box' + className;
  4764. dom.line.className = 'item line' + className;
  4765. dom.dot.className = 'item dot' + className;
  4766. changed = true;
  4767. }
  4768. }
  4769. return changed;
  4770. };
  4771. /**
  4772. * Show the item in the DOM (when not already visible). The items DOM will
  4773. * be created when needed.
  4774. * @return {Boolean} changed
  4775. */
  4776. ItemBox.prototype.show = function show() {
  4777. if (!this.dom || !this.dom.box.parentNode) {
  4778. return this.repaint();
  4779. }
  4780. else {
  4781. return false;
  4782. }
  4783. };
  4784. /**
  4785. * Hide the item from the DOM (when visible)
  4786. * @return {Boolean} changed
  4787. */
  4788. ItemBox.prototype.hide = function hide() {
  4789. var changed = false,
  4790. dom = this.dom;
  4791. if (dom) {
  4792. if (dom.box.parentNode) {
  4793. dom.box.parentNode.removeChild(dom.box);
  4794. changed = true;
  4795. }
  4796. if (dom.line.parentNode) {
  4797. dom.line.parentNode.removeChild(dom.line);
  4798. }
  4799. if (dom.dot.parentNode) {
  4800. dom.dot.parentNode.removeChild(dom.dot);
  4801. }
  4802. }
  4803. return changed;
  4804. };
  4805. /**
  4806. * Reflow the item: calculate its actual size and position from the DOM
  4807. * @return {boolean} resized returns true if the axis is resized
  4808. * @override
  4809. */
  4810. ItemBox.prototype.reflow = function reflow() {
  4811. var changed = 0,
  4812. update,
  4813. dom,
  4814. props,
  4815. options,
  4816. margin,
  4817. start,
  4818. align,
  4819. orientation,
  4820. top,
  4821. left,
  4822. data,
  4823. range;
  4824. if (this.data.start == undefined) {
  4825. throw new Error('Property "start" missing in item ' + this.data.id);
  4826. }
  4827. data = this.data;
  4828. range = this.parent && this.parent.range;
  4829. if (data && range) {
  4830. // TODO: account for the width of the item. Take some margin
  4831. this.visible = (data.start > range.start) && (data.start < range.end);
  4832. }
  4833. else {
  4834. this.visible = false;
  4835. }
  4836. if (this.visible) {
  4837. dom = this.dom;
  4838. if (dom) {
  4839. update = util.updateProperty;
  4840. props = this.props;
  4841. options = this.options;
  4842. start = this.parent.toScreen(this.data.start);
  4843. align = options.align || this.defaultOptions.align;
  4844. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  4845. orientation = options.orientation || this.defaultOptions.orientation;
  4846. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4847. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4848. changed += update(props.line, 'width', dom.line.offsetWidth);
  4849. changed += update(props.line, 'height', dom.line.offsetHeight);
  4850. changed += update(props.line, 'top', dom.line.offsetTop);
  4851. changed += update(this, 'width', dom.box.offsetWidth);
  4852. changed += update(this, 'height', dom.box.offsetHeight);
  4853. if (align == 'right') {
  4854. left = start - this.width;
  4855. }
  4856. else if (align == 'left') {
  4857. left = start;
  4858. }
  4859. else {
  4860. // default or 'center'
  4861. left = start - this.width / 2;
  4862. }
  4863. changed += update(this, 'left', left);
  4864. changed += update(props.line, 'left', start - props.line.width / 2);
  4865. changed += update(props.dot, 'left', start - props.dot.width / 2);
  4866. changed += update(props.dot, 'top', -props.dot.height / 2);
  4867. if (orientation == 'top') {
  4868. top = margin;
  4869. changed += update(this, 'top', top);
  4870. }
  4871. else {
  4872. // default or 'bottom'
  4873. var parentHeight = this.parent.height;
  4874. top = parentHeight - this.height - margin;
  4875. changed += update(this, 'top', top);
  4876. }
  4877. }
  4878. else {
  4879. changed += 1;
  4880. }
  4881. }
  4882. return (changed > 0);
  4883. };
  4884. /**
  4885. * Create an items DOM
  4886. * @private
  4887. */
  4888. ItemBox.prototype._create = function _create() {
  4889. var dom = this.dom;
  4890. if (!dom) {
  4891. this.dom = dom = {};
  4892. // create the box
  4893. dom.box = document.createElement('DIV');
  4894. // className is updated in repaint()
  4895. // contents box (inside the background box). used for making margins
  4896. dom.content = document.createElement('DIV');
  4897. dom.content.className = 'content';
  4898. dom.box.appendChild(dom.content);
  4899. // line to axis
  4900. dom.line = document.createElement('DIV');
  4901. dom.line.className = 'line';
  4902. // dot on axis
  4903. dom.dot = document.createElement('DIV');
  4904. dom.dot.className = 'dot';
  4905. }
  4906. };
  4907. /**
  4908. * Reposition the item, recalculate its left, top, and width, using the current
  4909. * range and size of the items itemset
  4910. * @override
  4911. */
  4912. ItemBox.prototype.reposition = function reposition() {
  4913. var dom = this.dom,
  4914. props = this.props,
  4915. orientation = this.options.orientation || this.defaultOptions.orientation;
  4916. if (dom) {
  4917. var box = dom.box,
  4918. line = dom.line,
  4919. dot = dom.dot;
  4920. box.style.left = this.left + 'px';
  4921. box.style.top = this.top + 'px';
  4922. line.style.left = props.line.left + 'px';
  4923. if (orientation == 'top') {
  4924. line.style.top = 0 + 'px';
  4925. line.style.height = this.top + 'px';
  4926. }
  4927. else {
  4928. // orientation 'bottom'
  4929. line.style.top = (this.top + this.height) + 'px';
  4930. line.style.height = Math.max(this.parent.height - this.top - this.height +
  4931. this.props.dot.height / 2, 0) + 'px';
  4932. }
  4933. dot.style.left = props.dot.left + 'px';
  4934. dot.style.top = props.dot.top + 'px';
  4935. }
  4936. };
  4937. /**
  4938. * @constructor ItemPoint
  4939. * @extends Item
  4940. * @param {ItemSet} parent
  4941. * @param {Object} data Object containing parameters start
  4942. * content, className.
  4943. * @param {Object} [options] Options to set initial property values
  4944. * @param {Object} [defaultOptions] default options
  4945. * // TODO: describe available options
  4946. */
  4947. function ItemPoint (parent, data, options, defaultOptions) {
  4948. this.props = {
  4949. dot: {
  4950. top: 0,
  4951. width: 0,
  4952. height: 0
  4953. },
  4954. content: {
  4955. height: 0,
  4956. marginLeft: 0
  4957. }
  4958. };
  4959. Item.call(this, parent, data, options, defaultOptions);
  4960. }
  4961. ItemPoint.prototype = new Item (null, null);
  4962. /**
  4963. * Select the item
  4964. * @override
  4965. */
  4966. ItemPoint.prototype.select = function select() {
  4967. this.selected = true;
  4968. // TODO: select and unselect
  4969. };
  4970. /**
  4971. * Unselect the item
  4972. * @override
  4973. */
  4974. ItemPoint.prototype.unselect = function unselect() {
  4975. this.selected = false;
  4976. // TODO: select and unselect
  4977. };
  4978. /**
  4979. * Repaint the item
  4980. * @return {Boolean} changed
  4981. */
  4982. ItemPoint.prototype.repaint = function repaint() {
  4983. // TODO: make an efficient repaint
  4984. var changed = false;
  4985. var dom = this.dom;
  4986. if (!dom) {
  4987. this._create();
  4988. dom = this.dom;
  4989. changed = true;
  4990. }
  4991. if (dom) {
  4992. if (!this.parent) {
  4993. throw new Error('Cannot repaint item: no parent attached');
  4994. }
  4995. var foreground = this.parent.getForeground();
  4996. if (!foreground) {
  4997. throw new Error('Cannot repaint time axis: ' +
  4998. 'parent has no foreground container element');
  4999. }
  5000. if (!dom.point.parentNode) {
  5001. foreground.appendChild(dom.point);
  5002. foreground.appendChild(dom.point);
  5003. changed = true;
  5004. }
  5005. // update contents
  5006. if (this.data.content != this.content) {
  5007. this.content = this.data.content;
  5008. if (this.content instanceof Element) {
  5009. dom.content.innerHTML = '';
  5010. dom.content.appendChild(this.content);
  5011. }
  5012. else if (this.data.content != undefined) {
  5013. dom.content.innerHTML = this.content;
  5014. }
  5015. else {
  5016. throw new Error('Property "content" missing in item ' + this.data.id);
  5017. }
  5018. changed = true;
  5019. }
  5020. // update class
  5021. var className = (this.data.className? ' ' + this.data.className : '') +
  5022. (this.selected ? ' selected' : '');
  5023. if (this.className != className) {
  5024. this.className = className;
  5025. dom.point.className = 'item point' + className;
  5026. changed = true;
  5027. }
  5028. }
  5029. return changed;
  5030. };
  5031. /**
  5032. * Show the item in the DOM (when not already visible). The items DOM will
  5033. * be created when needed.
  5034. * @return {Boolean} changed
  5035. */
  5036. ItemPoint.prototype.show = function show() {
  5037. if (!this.dom || !this.dom.point.parentNode) {
  5038. return this.repaint();
  5039. }
  5040. else {
  5041. return false;
  5042. }
  5043. };
  5044. /**
  5045. * Hide the item from the DOM (when visible)
  5046. * @return {Boolean} changed
  5047. */
  5048. ItemPoint.prototype.hide = function hide() {
  5049. var changed = false,
  5050. dom = this.dom;
  5051. if (dom) {
  5052. if (dom.point.parentNode) {
  5053. dom.point.parentNode.removeChild(dom.point);
  5054. changed = true;
  5055. }
  5056. }
  5057. return changed;
  5058. };
  5059. /**
  5060. * Reflow the item: calculate its actual size from the DOM
  5061. * @return {boolean} resized returns true if the axis is resized
  5062. * @override
  5063. */
  5064. ItemPoint.prototype.reflow = function reflow() {
  5065. var changed = 0,
  5066. update,
  5067. dom,
  5068. props,
  5069. options,
  5070. margin,
  5071. orientation,
  5072. start,
  5073. top,
  5074. data,
  5075. range;
  5076. if (this.data.start == undefined) {
  5077. throw new Error('Property "start" missing in item ' + this.data.id);
  5078. }
  5079. data = this.data;
  5080. range = this.parent && this.parent.range;
  5081. if (data && range) {
  5082. // TODO: account for the width of the item. Take some margin
  5083. this.visible = (data.start > range.start) && (data.start < range.end);
  5084. }
  5085. else {
  5086. this.visible = false;
  5087. }
  5088. if (this.visible) {
  5089. dom = this.dom;
  5090. if (dom) {
  5091. update = util.updateProperty;
  5092. props = this.props;
  5093. options = this.options;
  5094. orientation = options.orientation || this.defaultOptions.orientation;
  5095. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5096. start = this.parent.toScreen(this.data.start);
  5097. changed += update(this, 'width', dom.point.offsetWidth);
  5098. changed += update(this, 'height', dom.point.offsetHeight);
  5099. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5100. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5101. changed += update(props.content, 'height', dom.content.offsetHeight);
  5102. if (orientation == 'top') {
  5103. top = margin;
  5104. }
  5105. else {
  5106. // default or 'bottom'
  5107. var parentHeight = this.parent.height;
  5108. top = Math.max(parentHeight - this.height - margin, 0);
  5109. }
  5110. changed += update(this, 'top', top);
  5111. changed += update(this, 'left', start - props.dot.width / 2);
  5112. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5113. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5114. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5115. }
  5116. else {
  5117. changed += 1;
  5118. }
  5119. }
  5120. return (changed > 0);
  5121. };
  5122. /**
  5123. * Create an items DOM
  5124. * @private
  5125. */
  5126. ItemPoint.prototype._create = function _create() {
  5127. var dom = this.dom;
  5128. if (!dom) {
  5129. this.dom = dom = {};
  5130. // background box
  5131. dom.point = document.createElement('div');
  5132. // className is updated in repaint()
  5133. // contents box, right from the dot
  5134. dom.content = document.createElement('div');
  5135. dom.content.className = 'content';
  5136. dom.point.appendChild(dom.content);
  5137. // dot at start
  5138. dom.dot = document.createElement('div');
  5139. dom.dot.className = 'dot';
  5140. dom.point.appendChild(dom.dot);
  5141. }
  5142. };
  5143. /**
  5144. * Reposition the item, recalculate its left, top, and width, using the current
  5145. * range and size of the items itemset
  5146. * @override
  5147. */
  5148. ItemPoint.prototype.reposition = function reposition() {
  5149. var dom = this.dom,
  5150. props = this.props;
  5151. if (dom) {
  5152. dom.point.style.top = this.top + 'px';
  5153. dom.point.style.left = this.left + 'px';
  5154. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5155. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5156. dom.dot.style.top = props.dot.top + 'px';
  5157. }
  5158. };
  5159. /**
  5160. * @constructor ItemRange
  5161. * @extends Item
  5162. * @param {ItemSet} parent
  5163. * @param {Object} data Object containing parameters start, end
  5164. * content, className.
  5165. * @param {Object} [options] Options to set initial property values
  5166. * @param {Object} [defaultOptions] default options
  5167. * // TODO: describe available options
  5168. */
  5169. function ItemRange (parent, data, options, defaultOptions) {
  5170. this.props = {
  5171. content: {
  5172. left: 0,
  5173. width: 0
  5174. }
  5175. };
  5176. Item.call(this, parent, data, options, defaultOptions);
  5177. }
  5178. ItemRange.prototype = new Item (null, null);
  5179. /**
  5180. * Select the item
  5181. * @override
  5182. */
  5183. ItemRange.prototype.select = function select() {
  5184. this.selected = true;
  5185. // TODO: select and unselect
  5186. };
  5187. /**
  5188. * Unselect the item
  5189. * @override
  5190. */
  5191. ItemRange.prototype.unselect = function unselect() {
  5192. this.selected = false;
  5193. // TODO: select and unselect
  5194. };
  5195. /**
  5196. * Repaint the item
  5197. * @return {Boolean} changed
  5198. */
  5199. ItemRange.prototype.repaint = function repaint() {
  5200. // TODO: make an efficient repaint
  5201. var changed = false;
  5202. var dom = this.dom;
  5203. if (!dom) {
  5204. this._create();
  5205. dom = this.dom;
  5206. changed = true;
  5207. }
  5208. if (dom) {
  5209. if (!this.parent) {
  5210. throw new Error('Cannot repaint item: no parent attached');
  5211. }
  5212. var foreground = this.parent.getForeground();
  5213. if (!foreground) {
  5214. throw new Error('Cannot repaint time axis: ' +
  5215. 'parent has no foreground container element');
  5216. }
  5217. if (!dom.box.parentNode) {
  5218. foreground.appendChild(dom.box);
  5219. changed = true;
  5220. }
  5221. // update content
  5222. if (this.data.content != this.content) {
  5223. this.content = this.data.content;
  5224. if (this.content instanceof Element) {
  5225. dom.content.innerHTML = '';
  5226. dom.content.appendChild(this.content);
  5227. }
  5228. else if (this.data.content != undefined) {
  5229. dom.content.innerHTML = this.content;
  5230. }
  5231. else {
  5232. throw new Error('Property "content" missing in item ' + this.data.id);
  5233. }
  5234. changed = true;
  5235. }
  5236. // update class
  5237. var className = this.data.className ? ('' + this.data.className) : '';
  5238. if (this.className != className) {
  5239. this.className = className;
  5240. dom.box.className = 'item range' + className;
  5241. changed = true;
  5242. }
  5243. }
  5244. return changed;
  5245. };
  5246. /**
  5247. * Show the item in the DOM (when not already visible). The items DOM will
  5248. * be created when needed.
  5249. * @return {Boolean} changed
  5250. */
  5251. ItemRange.prototype.show = function show() {
  5252. if (!this.dom || !this.dom.box.parentNode) {
  5253. return this.repaint();
  5254. }
  5255. else {
  5256. return false;
  5257. }
  5258. };
  5259. /**
  5260. * Hide the item from the DOM (when visible)
  5261. * @return {Boolean} changed
  5262. */
  5263. ItemRange.prototype.hide = function hide() {
  5264. var changed = false,
  5265. dom = this.dom;
  5266. if (dom) {
  5267. if (dom.box.parentNode) {
  5268. dom.box.parentNode.removeChild(dom.box);
  5269. changed = true;
  5270. }
  5271. }
  5272. return changed;
  5273. };
  5274. /**
  5275. * Reflow the item: calculate its actual size from the DOM
  5276. * @return {boolean} resized returns true if the axis is resized
  5277. * @override
  5278. */
  5279. ItemRange.prototype.reflow = function reflow() {
  5280. var changed = 0,
  5281. dom,
  5282. props,
  5283. options,
  5284. margin,
  5285. padding,
  5286. parent,
  5287. start,
  5288. end,
  5289. data,
  5290. range,
  5291. update,
  5292. box,
  5293. parentWidth,
  5294. contentLeft,
  5295. orientation,
  5296. top;
  5297. if (this.data.start == undefined) {
  5298. throw new Error('Property "start" missing in item ' + this.data.id);
  5299. }
  5300. if (this.data.end == undefined) {
  5301. throw new Error('Property "end" missing in item ' + this.data.id);
  5302. }
  5303. data = this.data;
  5304. range = this.parent && this.parent.range;
  5305. if (data && range) {
  5306. // TODO: account for the width of the item. Take some margin
  5307. this.visible = (data.start < range.end) && (data.end > range.start);
  5308. }
  5309. else {
  5310. this.visible = false;
  5311. }
  5312. if (this.visible) {
  5313. dom = this.dom;
  5314. if (dom) {
  5315. props = this.props;
  5316. options = this.options;
  5317. parent = this.parent;
  5318. start = parent.toScreen(this.data.start);
  5319. end = parent.toScreen(this.data.end);
  5320. update = util.updateProperty;
  5321. box = dom.box;
  5322. parentWidth = parent.width;
  5323. orientation = options.orientation || this.defaultOptions.orientation;
  5324. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5325. padding = options.padding || this.defaultOptions.padding;
  5326. changed += update(props.content, 'width', dom.content.offsetWidth);
  5327. changed += update(this, 'height', box.offsetHeight);
  5328. // limit the width of the this, as browsers cannot draw very wide divs
  5329. if (start < -parentWidth) {
  5330. start = -parentWidth;
  5331. }
  5332. if (end > 2 * parentWidth) {
  5333. end = 2 * parentWidth;
  5334. }
  5335. // when range exceeds left of the window, position the contents at the left of the visible area
  5336. if (start < 0) {
  5337. contentLeft = Math.min(-start,
  5338. (end - start - props.content.width - 2 * padding));
  5339. // TODO: remove the need for options.padding. it's terrible.
  5340. }
  5341. else {
  5342. contentLeft = 0;
  5343. }
  5344. changed += update(props.content, 'left', contentLeft);
  5345. if (orientation == 'top') {
  5346. top = margin;
  5347. changed += update(this, 'top', top);
  5348. }
  5349. else {
  5350. // default or 'bottom'
  5351. top = parent.height - this.height - margin;
  5352. changed += update(this, 'top', top);
  5353. }
  5354. changed += update(this, 'left', start);
  5355. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5356. }
  5357. else {
  5358. changed += 1;
  5359. }
  5360. }
  5361. return (changed > 0);
  5362. };
  5363. /**
  5364. * Create an items DOM
  5365. * @private
  5366. */
  5367. ItemRange.prototype._create = function _create() {
  5368. var dom = this.dom;
  5369. if (!dom) {
  5370. this.dom = dom = {};
  5371. // background box
  5372. dom.box = document.createElement('div');
  5373. // className is updated in repaint()
  5374. // contents box
  5375. dom.content = document.createElement('div');
  5376. dom.content.className = 'content';
  5377. dom.box.appendChild(dom.content);
  5378. }
  5379. };
  5380. /**
  5381. * Reposition the item, recalculate its left, top, and width, using the current
  5382. * range and size of the items itemset
  5383. * @override
  5384. */
  5385. ItemRange.prototype.reposition = function reposition() {
  5386. var dom = this.dom,
  5387. props = this.props;
  5388. if (dom) {
  5389. dom.box.style.top = this.top + 'px';
  5390. dom.box.style.left = this.left + 'px';
  5391. dom.box.style.width = this.width + 'px';
  5392. dom.content.style.left = props.content.left + 'px';
  5393. }
  5394. };
  5395. /**
  5396. * @constructor Group
  5397. * @param {GroupSet} parent
  5398. * @param {Number | String} groupId
  5399. * @param {Object} [options] Options to set initial property values
  5400. * // TODO: describe available options
  5401. * @extends Component
  5402. */
  5403. function Group (parent, groupId, options) {
  5404. this.id = util.randomUUID();
  5405. this.parent = parent;
  5406. this.groupId = groupId;
  5407. this.itemsData = null; // DataSet
  5408. this.itemset = null; // ItemSet
  5409. this.options = options || {};
  5410. this.options.top = 0;
  5411. this.top = 0;
  5412. this.left = 0;
  5413. this.width = 0;
  5414. this.height = 0;
  5415. }
  5416. Group.prototype = new Component();
  5417. // TODO: comment
  5418. Group.prototype.setOptions = Component.prototype.setOptions;
  5419. /**
  5420. * Get the container element of the panel, which can be used by a child to
  5421. * add its own widgets.
  5422. * @returns {HTMLElement} container
  5423. */
  5424. Group.prototype.getContainer = function () {
  5425. return this.parent.getContainer();
  5426. };
  5427. /**
  5428. * Set item set for the group. The group will create a view on the itemset,
  5429. * filtered by the groups id.
  5430. * @param {DataSet | DataView} items
  5431. */
  5432. Group.prototype.setItems = function setItems(items) {
  5433. if (this.itemset) {
  5434. // remove current item set
  5435. this.itemset.hide();
  5436. this.itemset.setItems();
  5437. this.parent.controller.remove(this.itemset);
  5438. this.itemset = null;
  5439. }
  5440. if (items) {
  5441. var groupId = this.groupId;
  5442. var itemsetOptions = Object.create(this.options);
  5443. this.itemset = new ItemSet(this, null, itemsetOptions);
  5444. this.itemset.setRange(this.parent.range);
  5445. this.view = new DataView(items, {
  5446. filter: function (item) {
  5447. return item.group == groupId;
  5448. }
  5449. });
  5450. this.itemset.setItems(this.view);
  5451. this.parent.controller.add(this.itemset);
  5452. }
  5453. };
  5454. /**
  5455. * Repaint the item
  5456. * @return {Boolean} changed
  5457. */
  5458. Group.prototype.repaint = function repaint() {
  5459. return false;
  5460. };
  5461. /**
  5462. * Reflow the item
  5463. * @return {Boolean} resized
  5464. */
  5465. Group.prototype.reflow = function reflow() {
  5466. var changed = 0,
  5467. update = util.updateProperty;
  5468. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5469. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5470. return (changed > 0);
  5471. };
  5472. /**
  5473. * An GroupSet holds a set of groups
  5474. * @param {Component} parent
  5475. * @param {Component[]} [depends] Components on which this components depends
  5476. * (except for the parent)
  5477. * @param {Object} [options] See GroupSet.setOptions for the available
  5478. * options.
  5479. * @constructor GroupSet
  5480. * @extends Panel
  5481. */
  5482. function GroupSet(parent, depends, options) {
  5483. this.id = util.randomUUID();
  5484. this.parent = parent;
  5485. this.depends = depends;
  5486. this.options = options || {};
  5487. this.range = null; // Range or Object {start: number, end: number}
  5488. this.itemsData = null; // DataSet with items
  5489. this.groupsData = null; // DataSet with groups
  5490. this.groups = {}; // map with groups
  5491. // changes in groups are queued key/value map containing id/action
  5492. this.queue = {};
  5493. var me = this;
  5494. this.listeners = {
  5495. 'add': function (event, params) {
  5496. me._onAdd(params.items);
  5497. },
  5498. 'update': function (event, params) {
  5499. me._onUpdate(params.items);
  5500. },
  5501. 'remove': function (event, params) {
  5502. me._onRemove(params.items);
  5503. }
  5504. };
  5505. }
  5506. GroupSet.prototype = new Panel();
  5507. /**
  5508. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5509. * @param {Object} [options] The following options are available:
  5510. * {String | function} groupsOrder
  5511. * TODO: describe options
  5512. */
  5513. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5514. GroupSet.prototype.setRange = function (range) {
  5515. // TODO: implement setRange
  5516. };
  5517. /**
  5518. * Set items
  5519. * @param {vis.DataSet | null} items
  5520. */
  5521. GroupSet.prototype.setItems = function setItems(items) {
  5522. this.itemsData = items;
  5523. for (var id in this.groups) {
  5524. if (this.groups.hasOwnProperty(id)) {
  5525. var group = this.groups[id];
  5526. group.setItems(items);
  5527. }
  5528. }
  5529. };
  5530. /**
  5531. * Get items
  5532. * @return {vis.DataSet | null} items
  5533. */
  5534. GroupSet.prototype.getItems = function getItems() {
  5535. return this.itemsData;
  5536. };
  5537. /**
  5538. * Set range (start and end).
  5539. * @param {Range | Object} range A Range or an object containing start and end.
  5540. */
  5541. GroupSet.prototype.setRange = function setRange(range) {
  5542. this.range = range;
  5543. };
  5544. /**
  5545. * Set groups
  5546. * @param {vis.DataSet} groups
  5547. */
  5548. GroupSet.prototype.setGroups = function setGroups(groups) {
  5549. var me = this,
  5550. ids;
  5551. // unsubscribe from current dataset
  5552. if (this.groupsData) {
  5553. util.forEach(this.listeners, function (callback, event) {
  5554. me.groupsData.unsubscribe(event, callback);
  5555. });
  5556. // remove all drawn groups
  5557. ids = this.groupsData.getIds();
  5558. this._onRemove(ids);
  5559. }
  5560. // replace the dataset
  5561. if (!groups) {
  5562. this.groupsData = null;
  5563. }
  5564. else if (groups instanceof DataSet) {
  5565. this.groupsData = groups;
  5566. }
  5567. else {
  5568. this.groupsData = new DataSet({
  5569. fieldTypes: {
  5570. start: 'Date',
  5571. end: 'Date'
  5572. }
  5573. });
  5574. this.groupsData.add(groups);
  5575. }
  5576. if (this.groupsData) {
  5577. // subscribe to new dataset
  5578. var id = this.id;
  5579. util.forEach(this.listeners, function (callback, event) {
  5580. me.groupsData.subscribe(event, callback, id);
  5581. });
  5582. // draw all new groups
  5583. ids = this.groupsData.getIds();
  5584. this._onAdd(ids);
  5585. }
  5586. };
  5587. /**
  5588. * Get groups
  5589. * @return {vis.DataSet | null} groups
  5590. */
  5591. GroupSet.prototype.getGroups = function getGroups() {
  5592. return this.groupsData;
  5593. };
  5594. /**
  5595. * Repaint the component
  5596. * @return {Boolean} changed
  5597. */
  5598. GroupSet.prototype.repaint = function repaint() {
  5599. var changed = 0,
  5600. update = util.updateProperty,
  5601. asSize = util.option.asSize,
  5602. options = this.options,
  5603. frame = this.frame;
  5604. if (!frame) {
  5605. frame = document.createElement('div');
  5606. frame.className = 'groupset';
  5607. var className = options.className;
  5608. if (className) {
  5609. util.addClassName(frame, util.option.asString(className));
  5610. }
  5611. this.frame = frame;
  5612. changed += 1;
  5613. }
  5614. if (!this.parent) {
  5615. throw new Error('Cannot repaint groupset: no parent attached');
  5616. }
  5617. var parentContainer = this.parent.getContainer();
  5618. if (!parentContainer) {
  5619. throw new Error('Cannot repaint groupset: parent has no container element');
  5620. }
  5621. if (!frame.parentNode) {
  5622. parentContainer.appendChild(frame);
  5623. changed += 1;
  5624. }
  5625. // reposition frame
  5626. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  5627. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  5628. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  5629. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  5630. var me = this,
  5631. queue = this.queue,
  5632. groups = this.groups,
  5633. groupsData = this.groupsData;
  5634. // show/hide added/changed/removed items
  5635. var ids = Object.keys(queue);
  5636. if (ids.length) {
  5637. ids.forEach(function (id) {
  5638. var action = queue[id];
  5639. var group = groups[id];
  5640. //noinspection FallthroughInSwitchStatementJS
  5641. switch (action) {
  5642. case 'add':
  5643. case 'update':
  5644. if (!group) {
  5645. var groupOptions = Object.create(me.options);
  5646. group = new Group(me, id, groupOptions);
  5647. group.setItems(me.itemsData); // attach items data
  5648. groups[id] = group;
  5649. me.controller.add(group);
  5650. }
  5651. // TODO: update group data
  5652. group.data = groupsData.get(id);
  5653. delete queue[id];
  5654. break;
  5655. case 'remove':
  5656. if (group) {
  5657. group.setItems(); // detach items data
  5658. delete groups[id];
  5659. me.controller.remove(group);
  5660. }
  5661. // update lists
  5662. delete queue[id];
  5663. break;
  5664. default:
  5665. console.log('Error: unknown action "' + action + '"');
  5666. }
  5667. });
  5668. // the groupset depends on each of the groups
  5669. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  5670. // TODO: apply dependencies of the groupset
  5671. // update the top positions of the groups in the correct order
  5672. var orderedGroups = this.groupsData.getIds({
  5673. order: this.options.groupsOrder
  5674. });
  5675. for (var i = 0; i < orderedGroups.length; i++) {
  5676. (function (group, prevGroup) {
  5677. var top = 0;
  5678. if (prevGroup) {
  5679. top = function () {
  5680. // TODO: top must reckon with options.maxHeight
  5681. return prevGroup.top + prevGroup.height;
  5682. }
  5683. }
  5684. group.setOptions({
  5685. top: top
  5686. });
  5687. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  5688. }
  5689. changed++;
  5690. }
  5691. return (changed > 0);
  5692. };
  5693. /**
  5694. * Get container element
  5695. * @return {HTMLElement} container
  5696. */
  5697. GroupSet.prototype.getContainer = function getContainer() {
  5698. // TODO: replace later on with container element for holding itemsets
  5699. return this.frame;
  5700. };
  5701. /**
  5702. * Reflow the component
  5703. * @return {Boolean} resized
  5704. */
  5705. GroupSet.prototype.reflow = function reflow() {
  5706. var changed = 0,
  5707. options = this.options,
  5708. update = util.updateProperty,
  5709. asNumber = util.option.asNumber,
  5710. asSize = util.option.asSize,
  5711. frame = this.frame;
  5712. if (frame) {
  5713. var maxHeight = asNumber(options.maxHeight);
  5714. var fixedHeight = (asSize(options.height) != null);
  5715. var height;
  5716. if (fixedHeight) {
  5717. height = frame.offsetHeight;
  5718. }
  5719. else {
  5720. // height is not specified, calculate the sum of the height of all groups
  5721. height = 0;
  5722. for (var id in this.groups) {
  5723. if (this.groups.hasOwnProperty(id)) {
  5724. var group = this.groups[id];
  5725. height += group.height;
  5726. }
  5727. }
  5728. }
  5729. if (maxHeight != null) {
  5730. height = Math.min(height, maxHeight);
  5731. }
  5732. changed += update(this, 'height', height);
  5733. changed += update(this, 'top', frame.offsetTop);
  5734. changed += update(this, 'left', frame.offsetLeft);
  5735. changed += update(this, 'width', frame.offsetWidth);
  5736. }
  5737. return (changed > 0);
  5738. };
  5739. /**
  5740. * Hide the component from the DOM
  5741. * @return {Boolean} changed
  5742. */
  5743. GroupSet.prototype.hide = function hide() {
  5744. if (this.frame && this.frame.parentNode) {
  5745. this.frame.parentNode.removeChild(this.frame);
  5746. return true;
  5747. }
  5748. else {
  5749. return false;
  5750. }
  5751. };
  5752. /**
  5753. * Show the component in the DOM (when not already visible).
  5754. * A repaint will be executed when the component is not visible
  5755. * @return {Boolean} changed
  5756. */
  5757. GroupSet.prototype.show = function show() {
  5758. if (!this.frame || !this.frame.parentNode) {
  5759. return this.repaint();
  5760. }
  5761. else {
  5762. return false;
  5763. }
  5764. };
  5765. /**
  5766. * Handle updated groups
  5767. * @param {Number[]} ids
  5768. * @private
  5769. */
  5770. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  5771. this._toQueue(ids, 'update');
  5772. };
  5773. /**
  5774. * Handle changed groups
  5775. * @param {Number[]} ids
  5776. * @private
  5777. */
  5778. GroupSet.prototype._onAdd = function _onAdd(ids) {
  5779. this._toQueue(ids, 'add');
  5780. };
  5781. /**
  5782. * Handle removed groups
  5783. * @param {Number[]} ids
  5784. * @private
  5785. */
  5786. GroupSet.prototype._onRemove = function _onRemove(ids) {
  5787. this._toQueue(ids, 'remove');
  5788. };
  5789. /**
  5790. * Put groups in the queue to be added/updated/remove
  5791. * @param {Number[]} ids
  5792. * @param {String} action can be 'add', 'update', 'remove'
  5793. */
  5794. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  5795. var queue = this.queue;
  5796. ids.forEach(function (id) {
  5797. queue[id] = action;
  5798. });
  5799. if (this.controller) {
  5800. //this.requestReflow();
  5801. this.requestRepaint();
  5802. }
  5803. };
  5804. /**
  5805. * Create a timeline visualization
  5806. * @param {HTMLElement} container
  5807. * @param {vis.DataSet | Array | DataTable} [items]
  5808. * @param {Object} [options] See Timeline.setOptions for the available options.
  5809. * @constructor
  5810. */
  5811. function Timeline (container, items, options) {
  5812. var me = this;
  5813. this.options = util.extend({
  5814. orientation: 'bottom',
  5815. min: null,
  5816. max: null,
  5817. zoomMin: 10, // milliseconds
  5818. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  5819. // moveable: true, // TODO: option moveable
  5820. // zoomable: true, // TODO: option zoomable
  5821. showMinorLabels: true,
  5822. showMajorLabels: true,
  5823. autoResize: false
  5824. }, options);
  5825. // controller
  5826. this.controller = new Controller();
  5827. // root panel
  5828. if (!container) {
  5829. throw new Error('No container element provided');
  5830. }
  5831. var mainOptions = Object.create(this.options);
  5832. mainOptions.height = function () {
  5833. if (me.options.height) {
  5834. // fixed height
  5835. return me.options.height;
  5836. }
  5837. else {
  5838. // auto height
  5839. return me.timeaxis.height + me.content.height;
  5840. }
  5841. };
  5842. this.root = new RootPanel(container, mainOptions);
  5843. this.controller.add(this.root);
  5844. // range
  5845. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  5846. this.range = new Range({
  5847. start: now.clone().add('days', -3).valueOf(),
  5848. end: now.clone().add('days', 4).valueOf()
  5849. });
  5850. // TODO: reckon with options moveable and zoomable
  5851. this.range.subscribe(this.root, 'move', 'horizontal');
  5852. this.range.subscribe(this.root, 'zoom', 'horizontal');
  5853. this.range.on('rangechange', function () {
  5854. var force = true;
  5855. me.controller.requestReflow(force);
  5856. });
  5857. this.range.on('rangechanged', function () {
  5858. var force = true;
  5859. me.controller.requestReflow(force);
  5860. });
  5861. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  5862. // time axis
  5863. var timeaxisOptions = Object.create(mainOptions);
  5864. timeaxisOptions.range = this.range;
  5865. this.timeaxis = new TimeAxis(this.root, [], timeaxisOptions);
  5866. this.timeaxis.setRange(this.range);
  5867. this.controller.add(this.timeaxis);
  5868. // create itemset or groupset
  5869. this.setGroups(null);
  5870. this.itemsData = null; // DataSet
  5871. this.groupsData = null; // DataSet
  5872. // set data
  5873. if (items) {
  5874. this.setItems(items);
  5875. }
  5876. }
  5877. /**
  5878. * Set options
  5879. * @param {Object} options TODO: describe the available options
  5880. */
  5881. Timeline.prototype.setOptions = function (options) {
  5882. if (options) {
  5883. util.extend(this.options, options);
  5884. }
  5885. this.controller.reflow();
  5886. this.controller.repaint();
  5887. };
  5888. /**
  5889. * Set items
  5890. * @param {vis.DataSet | Array | DataTable | null} items
  5891. */
  5892. Timeline.prototype.setItems = function(items) {
  5893. var initialLoad = (this.itemsData == null);
  5894. // convert to type DataSet when needed
  5895. var newItemSet;
  5896. if (!items) {
  5897. newItemSet = null;
  5898. }
  5899. else if (items instanceof DataSet) {
  5900. newItemSet = items;
  5901. }
  5902. if (!(items instanceof DataSet)) {
  5903. newItemSet = new DataSet({
  5904. fieldTypes: {
  5905. start: 'Date',
  5906. end: 'Date'
  5907. }
  5908. });
  5909. newItemSet.add(items);
  5910. }
  5911. // set items
  5912. this.itemsData = newItemSet;
  5913. this.content.setItems(newItemSet);
  5914. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  5915. // apply the data range as range
  5916. var dataRange = this.getItemRange();
  5917. // add 5% on both sides
  5918. var min = dataRange.min;
  5919. var max = dataRange.max;
  5920. if (min != null && max != null) {
  5921. var interval = (max.valueOf() - min.valueOf());
  5922. min = new Date(min.valueOf() - interval * 0.05);
  5923. max = new Date(max.valueOf() + interval * 0.05);
  5924. }
  5925. // override specified start and/or end date
  5926. if (this.options.start != undefined) {
  5927. min = new Date(this.options.start.valueOf());
  5928. }
  5929. if (this.options.end != undefined) {
  5930. max = new Date(this.options.end.valueOf());
  5931. }
  5932. // apply range if there is a min or max available
  5933. if (min != null || max != null) {
  5934. this.range.setRange(min, max);
  5935. }
  5936. }
  5937. };
  5938. /**
  5939. * Set groups
  5940. * @param {vis.DataSet | Array | DataTable} groups
  5941. */
  5942. Timeline.prototype.setGroups = function(groups) {
  5943. var me = this;
  5944. this.groupsData = groups;
  5945. // switch content type between ItemSet or GroupSet when needed
  5946. var type = this.groupsData ? GroupSet : ItemSet;
  5947. if (!(this.content instanceof type)) {
  5948. // remove old content set
  5949. if (this.content) {
  5950. this.content.hide();
  5951. if (this.content.setItems) {
  5952. this.content.setItems(); // disconnect from items
  5953. }
  5954. if (this.content.setGroups) {
  5955. this.content.setGroups(); // disconnect from groups
  5956. }
  5957. this.controller.remove(this.content);
  5958. }
  5959. // create new content set
  5960. var options = Object.create(this.options);
  5961. util.extend(options, {
  5962. top: function () {
  5963. if (me.options.orientation == 'top') {
  5964. return me.timeaxis.height;
  5965. }
  5966. else {
  5967. return me.root.height - me.timeaxis.height - me.content.height;
  5968. }
  5969. },
  5970. height: function () {
  5971. if (me.options.height) {
  5972. return me.root.height - me.timeaxis.height;
  5973. }
  5974. else {
  5975. return null;
  5976. }
  5977. },
  5978. maxHeight: function () {
  5979. if (me.options.maxHeight) {
  5980. if (!util.isNumber(me.options.maxHeight)) {
  5981. throw new TypeError('Number expected for property maxHeight');
  5982. }
  5983. return me.options.maxHeight - me.timeaxis.height;
  5984. }
  5985. else {
  5986. return null;
  5987. }
  5988. }
  5989. });
  5990. this.content = new type(this.root, [this.timeaxis], options);
  5991. if (this.content.setRange) {
  5992. this.content.setRange(this.range);
  5993. }
  5994. if (this.content.setItems) {
  5995. this.content.setItems(this.itemsData);
  5996. }
  5997. if (this.content.setGroups) {
  5998. this.content.setGroups(this.groupsData);
  5999. }
  6000. this.controller.add(this.content);
  6001. }
  6002. };
  6003. /**
  6004. * Get the data range of the item set.
  6005. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6006. * When no minimum is found, min==null
  6007. * When no maximum is found, max==null
  6008. */
  6009. Timeline.prototype.getItemRange = function getItemRange() {
  6010. // calculate min from start filed
  6011. var itemsData = this.itemsData,
  6012. min = null,
  6013. max = null;
  6014. if (itemsData) {
  6015. // calculate the minimum value of the field 'start'
  6016. var minItem = itemsData.min('start');
  6017. min = minItem ? minItem.start.valueOf() : null;
  6018. // calculate maximum value of fields 'start' and 'end'
  6019. var maxStartItem = itemsData.max('start');
  6020. if (maxStartItem) {
  6021. max = maxStartItem.start.valueOf();
  6022. }
  6023. var maxEndItem = itemsData.max('end');
  6024. if (maxEndItem) {
  6025. if (max == null) {
  6026. max = maxEndItem.end.valueOf();
  6027. }
  6028. else {
  6029. max = Math.max(max, maxEndItem.end.valueOf());
  6030. }
  6031. }
  6032. }
  6033. return {
  6034. min: (min != null) ? new Date(min) : null,
  6035. max: (max != null) ? new Date(max) : null
  6036. };
  6037. };
  6038. (function(exports) {
  6039. /**
  6040. * Parse a text source containing data in DOT language into a JSON object.
  6041. * The object contains two lists: one with nodes and one with edges.
  6042. *
  6043. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6044. *
  6045. * @param {String} data Text containing a graph in DOT-notation
  6046. * @return {Object} graph An object containing two parameters:
  6047. * {Object[]} nodes
  6048. * {Object[]} edges
  6049. */
  6050. function parseDOT (data) {
  6051. dot = data;
  6052. return parseGraph();
  6053. }
  6054. // token types enumeration
  6055. var TOKENTYPE = {
  6056. NULL : 0,
  6057. DELIMITER : 1,
  6058. IDENTIFIER: 2,
  6059. UNKNOWN : 3
  6060. };
  6061. // map with all delimiters
  6062. var DELIMITERS = {
  6063. '{': true,
  6064. '}': true,
  6065. '[': true,
  6066. ']': true,
  6067. ';': true,
  6068. '=': true,
  6069. ',': true,
  6070. '->': true,
  6071. '--': true
  6072. };
  6073. var dot = ''; // current dot file
  6074. var index = 0; // current index in dot file
  6075. var c = ''; // current token character in expr
  6076. var token = ''; // current token
  6077. var tokenType = TOKENTYPE.NULL; // type of the token
  6078. /**
  6079. * Get the first character from the dot file.
  6080. * The character is stored into the char c. If the end of the dot file is
  6081. * reached, the function puts an empty string in c.
  6082. */
  6083. function first() {
  6084. index = 0;
  6085. c = dot.charAt(0);
  6086. }
  6087. /**
  6088. * Get the next character from the dot file.
  6089. * The character is stored into the char c. If the end of the dot file is
  6090. * reached, the function puts an empty string in c.
  6091. */
  6092. function next() {
  6093. index++;
  6094. c = dot.charAt(index);
  6095. }
  6096. /**
  6097. * Preview the next character from the dot file.
  6098. * @return {String} cNext
  6099. */
  6100. function nextPreview() {
  6101. return dot.charAt(index + 1);
  6102. }
  6103. /**
  6104. * Test whether given character is alphabetic or numeric
  6105. * @param {String} c
  6106. * @return {Boolean} isAlphaNumeric
  6107. */
  6108. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6109. function isAlphaNumeric(c) {
  6110. return regexAlphaNumeric.test(c);
  6111. }
  6112. /**
  6113. * Merge all properties of object b into object b
  6114. * @param {Object} a
  6115. * @param {Object} b
  6116. * @return {Object} a
  6117. */
  6118. function merge (a, b) {
  6119. if (!a) {
  6120. a = {};
  6121. }
  6122. if (b) {
  6123. for (var name in b) {
  6124. if (b.hasOwnProperty(name)) {
  6125. a[name] = b[name];
  6126. }
  6127. }
  6128. }
  6129. return a;
  6130. }
  6131. /**
  6132. * Set a value in an object, where the provided parameter name can be a
  6133. * path with nested parameters. For example:
  6134. *
  6135. * var obj = {a: 2};
  6136. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6137. *
  6138. * @param {Object} obj
  6139. * @param {String} path A parameter name or dot-separated parameter path,
  6140. * like "color.highlight.border".
  6141. * @param {*} value
  6142. */
  6143. function setValue(obj, path, value) {
  6144. var keys = path.split('.');
  6145. var o = obj;
  6146. while (keys.length) {
  6147. var key = keys.shift();
  6148. if (keys.length) {
  6149. // this isn't the end point
  6150. if (!o[key]) {
  6151. o[key] = {};
  6152. }
  6153. o = o[key];
  6154. }
  6155. else {
  6156. // this is the end point
  6157. o[key] = value;
  6158. }
  6159. }
  6160. }
  6161. /**
  6162. * Add a node to a graph object. If there is already a node with
  6163. * the same id, their attributes will be merged.
  6164. * @param {Object} graph
  6165. * @param {Object} node
  6166. */
  6167. function addNode(graph, node) {
  6168. var i, len;
  6169. var current = null;
  6170. // find root graph (in case of subgraph)
  6171. var graphs = [graph]; // list with all graphs from current graph to root graph
  6172. var root = graph;
  6173. while (root.parent) {
  6174. graphs.push(root.parent);
  6175. root = root.parent;
  6176. }
  6177. // find existing node (at root level) by its id
  6178. if (root.nodes) {
  6179. for (i = 0, len = root.nodes.length; i < len; i++) {
  6180. if (node.id === root.nodes[i].id) {
  6181. current = root.nodes[i];
  6182. break;
  6183. }
  6184. }
  6185. }
  6186. if (!current) {
  6187. // this is a new node
  6188. current = {
  6189. id: node.id
  6190. };
  6191. if (graph.node) {
  6192. // clone default attributes
  6193. current.attr = merge(current.attr, graph.node);
  6194. }
  6195. }
  6196. // add node to this (sub)graph and all its parent graphs
  6197. for (i = graphs.length - 1; i >= 0; i--) {
  6198. var g = graphs[i];
  6199. if (!g.nodes) {
  6200. g.nodes = [];
  6201. }
  6202. if (g.nodes.indexOf(current) == -1) {
  6203. g.nodes.push(current);
  6204. }
  6205. }
  6206. // merge attributes
  6207. if (node.attr) {
  6208. current.attr = merge(current.attr, node.attr);
  6209. }
  6210. }
  6211. /**
  6212. * Add an edge to a graph object
  6213. * @param {Object} graph
  6214. * @param {Object} edge
  6215. */
  6216. function addEdge(graph, edge) {
  6217. if (!graph.edges) {
  6218. graph.edges = [];
  6219. }
  6220. graph.edges.push(edge);
  6221. if (graph.edge) {
  6222. var attr = merge({}, graph.edge); // clone default attributes
  6223. edge.attr = merge(attr, edge.attr); // merge attributes
  6224. }
  6225. }
  6226. /**
  6227. * Create an edge to a graph object
  6228. * @param {Object} graph
  6229. * @param {String | Number | Object} from
  6230. * @param {String | Number | Object} to
  6231. * @param {String} type
  6232. * @param {Object | null} attr
  6233. * @return {Object} edge
  6234. */
  6235. function createEdge(graph, from, to, type, attr) {
  6236. var edge = {
  6237. from: from,
  6238. to: to,
  6239. type: type
  6240. };
  6241. if (graph.edge) {
  6242. edge.attr = merge({}, graph.edge); // clone default attributes
  6243. }
  6244. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  6245. return edge;
  6246. }
  6247. /**
  6248. * Get next token in the current dot file.
  6249. * The token and token type are available as token and tokenType
  6250. */
  6251. function getToken() {
  6252. tokenType = TOKENTYPE.NULL;
  6253. token = '';
  6254. // skip over whitespaces
  6255. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6256. next();
  6257. }
  6258. do {
  6259. var isComment = false;
  6260. // skip comment
  6261. if (c == '#') {
  6262. // find the previous non-space character
  6263. var i = index - 1;
  6264. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  6265. i--;
  6266. }
  6267. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  6268. // the # is at the start of a line, this is indeed a line comment
  6269. while (c != '' && c != '\n') {
  6270. next();
  6271. }
  6272. isComment = true;
  6273. }
  6274. }
  6275. if (c == '/' && nextPreview() == '/') {
  6276. // skip line comment
  6277. while (c != '' && c != '\n') {
  6278. next();
  6279. }
  6280. isComment = true;
  6281. }
  6282. if (c == '/' && nextPreview() == '*') {
  6283. // skip block comment
  6284. while (c != '') {
  6285. if (c == '*' && nextPreview() == '/') {
  6286. // end of block comment found. skip these last two characters
  6287. next();
  6288. next();
  6289. break;
  6290. }
  6291. else {
  6292. next();
  6293. }
  6294. }
  6295. isComment = true;
  6296. }
  6297. // skip over whitespaces
  6298. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6299. next();
  6300. }
  6301. }
  6302. while (isComment);
  6303. // check for end of dot file
  6304. if (c == '') {
  6305. // token is still empty
  6306. tokenType = TOKENTYPE.DELIMITER;
  6307. return;
  6308. }
  6309. // check for delimiters consisting of 2 characters
  6310. var c2 = c + nextPreview();
  6311. if (DELIMITERS[c2]) {
  6312. tokenType = TOKENTYPE.DELIMITER;
  6313. token = c2;
  6314. next();
  6315. next();
  6316. return;
  6317. }
  6318. // check for delimiters consisting of 1 character
  6319. if (DELIMITERS[c]) {
  6320. tokenType = TOKENTYPE.DELIMITER;
  6321. token = c;
  6322. next();
  6323. return;
  6324. }
  6325. // check for an identifier (number or string)
  6326. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  6327. if (isAlphaNumeric(c) || c == '-') {
  6328. token += c;
  6329. next();
  6330. while (isAlphaNumeric(c)) {
  6331. token += c;
  6332. next();
  6333. }
  6334. if (token == 'false') {
  6335. token = false; // cast to boolean
  6336. }
  6337. else if (token == 'true') {
  6338. token = true; // cast to boolean
  6339. }
  6340. else if (!isNaN(Number(token))) {
  6341. token = Number(token); // cast to number
  6342. }
  6343. tokenType = TOKENTYPE.IDENTIFIER;
  6344. return;
  6345. }
  6346. // check for a string enclosed by double quotes
  6347. if (c == '"') {
  6348. next();
  6349. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  6350. token += c;
  6351. if (c == '"') { // skip the escape character
  6352. next();
  6353. }
  6354. next();
  6355. }
  6356. if (c != '"') {
  6357. throw newSyntaxError('End of string " expected');
  6358. }
  6359. next();
  6360. tokenType = TOKENTYPE.IDENTIFIER;
  6361. return;
  6362. }
  6363. // something unknown is found, wrong characters, a syntax error
  6364. tokenType = TOKENTYPE.UNKNOWN;
  6365. while (c != '') {
  6366. token += c;
  6367. next();
  6368. }
  6369. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  6370. }
  6371. /**
  6372. * Parse a graph.
  6373. * @returns {Object} graph
  6374. */
  6375. function parseGraph() {
  6376. var graph = {};
  6377. first();
  6378. getToken();
  6379. // optional strict keyword
  6380. if (token == 'strict') {
  6381. graph.strict = true;
  6382. getToken();
  6383. }
  6384. // graph or digraph keyword
  6385. if (token == 'graph' || token == 'digraph') {
  6386. graph.type = token;
  6387. getToken();
  6388. }
  6389. // optional graph id
  6390. if (tokenType == TOKENTYPE.IDENTIFIER) {
  6391. graph.id = token;
  6392. getToken();
  6393. }
  6394. // open angle bracket
  6395. if (token != '{') {
  6396. throw newSyntaxError('Angle bracket { expected');
  6397. }
  6398. getToken();
  6399. // statements
  6400. parseStatements(graph);
  6401. // close angle bracket
  6402. if (token != '}') {
  6403. throw newSyntaxError('Angle bracket } expected');
  6404. }
  6405. getToken();
  6406. // end of file
  6407. if (token !== '') {
  6408. throw newSyntaxError('End of file expected');
  6409. }
  6410. getToken();
  6411. // remove temporary default properties
  6412. delete graph.node;
  6413. delete graph.edge;
  6414. delete graph.graph;
  6415. return graph;
  6416. }
  6417. /**
  6418. * Parse a list with statements.
  6419. * @param {Object} graph
  6420. */
  6421. function parseStatements (graph) {
  6422. while (token !== '' && token != '}') {
  6423. parseStatement(graph);
  6424. if (token == ';') {
  6425. getToken();
  6426. }
  6427. }
  6428. }
  6429. /**
  6430. * Parse a single statement. Can be a an attribute statement, node
  6431. * statement, a series of node statements and edge statements, or a
  6432. * parameter.
  6433. * @param {Object} graph
  6434. */
  6435. function parseStatement(graph) {
  6436. // parse subgraph
  6437. var subgraph = parseSubgraph(graph);
  6438. if (subgraph) {
  6439. // edge statements
  6440. parseEdge(graph, subgraph);
  6441. return;
  6442. }
  6443. // parse an attribute statement
  6444. var attr = parseAttributeStatement(graph);
  6445. if (attr) {
  6446. return;
  6447. }
  6448. // parse node
  6449. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6450. throw newSyntaxError('Identifier expected');
  6451. }
  6452. var id = token; // id can be a string or a number
  6453. getToken();
  6454. if (token == '=') {
  6455. // id statement
  6456. getToken();
  6457. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6458. throw newSyntaxError('Identifier expected');
  6459. }
  6460. graph[id] = token;
  6461. getToken();
  6462. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  6463. }
  6464. else {
  6465. parseNodeStatement(graph, id);
  6466. }
  6467. }
  6468. /**
  6469. * Parse a subgraph
  6470. * @param {Object} graph parent graph object
  6471. * @return {Object | null} subgraph
  6472. */
  6473. function parseSubgraph (graph) {
  6474. var subgraph = null;
  6475. // optional subgraph keyword
  6476. if (token == 'subgraph') {
  6477. subgraph = {};
  6478. subgraph.type = 'subgraph';
  6479. getToken();
  6480. // optional graph id
  6481. if (tokenType == TOKENTYPE.IDENTIFIER) {
  6482. subgraph.id = token;
  6483. getToken();
  6484. }
  6485. }
  6486. // open angle bracket
  6487. if (token == '{') {
  6488. getToken();
  6489. if (!subgraph) {
  6490. subgraph = {};
  6491. }
  6492. subgraph.parent = graph;
  6493. subgraph.node = graph.node;
  6494. subgraph.edge = graph.edge;
  6495. subgraph.graph = graph.graph;
  6496. // statements
  6497. parseStatements(subgraph);
  6498. // close angle bracket
  6499. if (token != '}') {
  6500. throw newSyntaxError('Angle bracket } expected');
  6501. }
  6502. getToken();
  6503. // remove temporary default properties
  6504. delete subgraph.node;
  6505. delete subgraph.edge;
  6506. delete subgraph.graph;
  6507. delete subgraph.parent;
  6508. // register at the parent graph
  6509. if (!graph.subgraphs) {
  6510. graph.subgraphs = [];
  6511. }
  6512. graph.subgraphs.push(subgraph);
  6513. }
  6514. return subgraph;
  6515. }
  6516. /**
  6517. * parse an attribute statement like "node [shape=circle fontSize=16]".
  6518. * Available keywords are 'node', 'edge', 'graph'.
  6519. * The previous list with default attributes will be replaced
  6520. * @param {Object} graph
  6521. * @returns {String | null} keyword Returns the name of the parsed attribute
  6522. * (node, edge, graph), or null if nothing
  6523. * is parsed.
  6524. */
  6525. function parseAttributeStatement (graph) {
  6526. // attribute statements
  6527. if (token == 'node') {
  6528. getToken();
  6529. // node attributes
  6530. graph.node = parseAttributeList();
  6531. return 'node';
  6532. }
  6533. else if (token == 'edge') {
  6534. getToken();
  6535. // edge attributes
  6536. graph.edge = parseAttributeList();
  6537. return 'edge';
  6538. }
  6539. else if (token == 'graph') {
  6540. getToken();
  6541. // graph attributes
  6542. graph.graph = parseAttributeList();
  6543. return 'graph';
  6544. }
  6545. return null;
  6546. }
  6547. /**
  6548. * parse a node statement
  6549. * @param {Object} graph
  6550. * @param {String | Number} id
  6551. */
  6552. function parseNodeStatement(graph, id) {
  6553. // node statement
  6554. var node = {
  6555. id: id
  6556. };
  6557. var attr = parseAttributeList();
  6558. if (attr) {
  6559. node.attr = attr;
  6560. }
  6561. addNode(graph, node);
  6562. // edge statements
  6563. parseEdge(graph, id);
  6564. }
  6565. /**
  6566. * Parse an edge or a series of edges
  6567. * @param {Object} graph
  6568. * @param {String | Number} from Id of the from node
  6569. */
  6570. function parseEdge(graph, from) {
  6571. while (token == '->' || token == '--') {
  6572. var to;
  6573. var type = token;
  6574. getToken();
  6575. var subgraph = parseSubgraph(graph);
  6576. if (subgraph) {
  6577. to = subgraph;
  6578. }
  6579. else {
  6580. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6581. throw newSyntaxError('Identifier or subgraph expected');
  6582. }
  6583. to = token;
  6584. addNode(graph, {
  6585. id: to
  6586. });
  6587. getToken();
  6588. }
  6589. // parse edge attributes
  6590. var attr = parseAttributeList();
  6591. // create edge
  6592. var edge = createEdge(graph, from, to, type, attr);
  6593. addEdge(graph, edge);
  6594. from = to;
  6595. }
  6596. }
  6597. /**
  6598. * Parse a set with attributes,
  6599. * for example [label="1.000", shape=solid]
  6600. * @return {Object | null} attr
  6601. */
  6602. function parseAttributeList() {
  6603. var attr = null;
  6604. while (token == '[') {
  6605. getToken();
  6606. attr = {};
  6607. while (token !== '' && token != ']') {
  6608. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6609. throw newSyntaxError('Attribute name expected');
  6610. }
  6611. var name = token;
  6612. getToken();
  6613. if (token != '=') {
  6614. throw newSyntaxError('Equal sign = expected');
  6615. }
  6616. getToken();
  6617. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6618. throw newSyntaxError('Attribute value expected');
  6619. }
  6620. var value = token;
  6621. setValue(attr, name, value); // name can be a path
  6622. getToken();
  6623. if (token ==',') {
  6624. getToken();
  6625. }
  6626. }
  6627. if (token != ']') {
  6628. throw newSyntaxError('Bracket ] expected');
  6629. }
  6630. getToken();
  6631. }
  6632. return attr;
  6633. }
  6634. /**
  6635. * Create a syntax error with extra information on current token and index.
  6636. * @param {String} message
  6637. * @returns {SyntaxError} err
  6638. */
  6639. function newSyntaxError(message) {
  6640. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  6641. }
  6642. /**
  6643. * Chop off text after a maximum length
  6644. * @param {String} text
  6645. * @param {Number} maxLength
  6646. * @returns {String}
  6647. */
  6648. function chop (text, maxLength) {
  6649. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  6650. }
  6651. /**
  6652. * Execute a function fn for each pair of elements in two arrays
  6653. * @param {Array | *} array1
  6654. * @param {Array | *} array2
  6655. * @param {function} fn
  6656. */
  6657. function forEach2(array1, array2, fn) {
  6658. if (array1 instanceof Array) {
  6659. array1.forEach(function (elem1) {
  6660. if (array2 instanceof Array) {
  6661. array2.forEach(function (elem2) {
  6662. fn(elem1, elem2);
  6663. });
  6664. }
  6665. else {
  6666. fn(elem1, array2);
  6667. }
  6668. });
  6669. }
  6670. else {
  6671. if (array2 instanceof Array) {
  6672. array2.forEach(function (elem2) {
  6673. fn(array1, elem2);
  6674. });
  6675. }
  6676. else {
  6677. fn(array1, array2);
  6678. }
  6679. }
  6680. }
  6681. /**
  6682. * Convert a string containing a graph in DOT language into a map containing
  6683. * with nodes and edges in the format of graph.
  6684. * @param {String} data Text containing a graph in DOT-notation
  6685. * @return {Object} graphData
  6686. */
  6687. function DOTToGraph (data) {
  6688. // parse the DOT file
  6689. var dotData = parseDOT(data);
  6690. var graphData = {
  6691. nodes: [],
  6692. edges: [],
  6693. options: {}
  6694. };
  6695. // copy the nodes
  6696. if (dotData.nodes) {
  6697. dotData.nodes.forEach(function (dotNode) {
  6698. var graphNode = {
  6699. id: dotNode.id,
  6700. label: String(dotNode.label || dotNode.id)
  6701. };
  6702. merge(graphNode, dotNode.attr);
  6703. if (graphNode.image) {
  6704. graphNode.shape = 'image';
  6705. }
  6706. graphData.nodes.push(graphNode);
  6707. });
  6708. }
  6709. // copy the edges
  6710. if (dotData.edges) {
  6711. /**
  6712. * Convert an edge in DOT format to an edge with VisGraph format
  6713. * @param {Object} dotEdge
  6714. * @returns {Object} graphEdge
  6715. */
  6716. function convertEdge(dotEdge) {
  6717. var graphEdge = {
  6718. from: dotEdge.from,
  6719. to: dotEdge.to
  6720. };
  6721. merge(graphEdge, dotEdge.attr);
  6722. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  6723. return graphEdge;
  6724. }
  6725. dotData.edges.forEach(function (dotEdge) {
  6726. var from, to;
  6727. if (dotEdge.from instanceof Object) {
  6728. from = dotEdge.from.nodes;
  6729. }
  6730. else {
  6731. from = {
  6732. id: dotEdge.from
  6733. }
  6734. }
  6735. if (dotEdge.to instanceof Object) {
  6736. to = dotEdge.to.nodes;
  6737. }
  6738. else {
  6739. to = {
  6740. id: dotEdge.to
  6741. }
  6742. }
  6743. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  6744. dotEdge.from.edges.forEach(function (subEdge) {
  6745. var graphEdge = convertEdge(subEdge);
  6746. graphData.edges.push(graphEdge);
  6747. });
  6748. }
  6749. forEach2(from, to, function (from, to) {
  6750. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  6751. var graphEdge = convertEdge(subEdge);
  6752. graphData.edges.push(graphEdge);
  6753. });
  6754. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  6755. dotEdge.to.edges.forEach(function (subEdge) {
  6756. var graphEdge = convertEdge(subEdge);
  6757. graphData.edges.push(graphEdge);
  6758. });
  6759. }
  6760. });
  6761. }
  6762. // copy the options
  6763. if (dotData.attr) {
  6764. graphData.options = dotData.attr;
  6765. }
  6766. return graphData;
  6767. }
  6768. // exports
  6769. exports.parseDOT = parseDOT;
  6770. exports.DOTToGraph = DOTToGraph;
  6771. })(typeof util !== 'undefined' ? util : exports);
  6772. /**
  6773. * Canvas shapes used by the Graph
  6774. */
  6775. if (typeof CanvasRenderingContext2D !== 'undefined') {
  6776. /**
  6777. * Draw a circle shape
  6778. */
  6779. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  6780. this.beginPath();
  6781. this.arc(x, y, r, 0, 2*Math.PI, false);
  6782. };
  6783. /**
  6784. * Draw a square shape
  6785. * @param {Number} x horizontal center
  6786. * @param {Number} y vertical center
  6787. * @param {Number} r size, width and height of the square
  6788. */
  6789. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  6790. this.beginPath();
  6791. this.rect(x - r, y - r, r * 2, r * 2);
  6792. };
  6793. /**
  6794. * Draw a triangle shape
  6795. * @param {Number} x horizontal center
  6796. * @param {Number} y vertical center
  6797. * @param {Number} r radius, half the length of the sides of the triangle
  6798. */
  6799. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  6800. // http://en.wikipedia.org/wiki/Equilateral_triangle
  6801. this.beginPath();
  6802. var s = r * 2;
  6803. var s2 = s / 2;
  6804. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  6805. var h = Math.sqrt(s * s - s2 * s2); // height
  6806. this.moveTo(x, y - (h - ir));
  6807. this.lineTo(x + s2, y + ir);
  6808. this.lineTo(x - s2, y + ir);
  6809. this.lineTo(x, y - (h - ir));
  6810. this.closePath();
  6811. };
  6812. /**
  6813. * Draw a triangle shape in downward orientation
  6814. * @param {Number} x horizontal center
  6815. * @param {Number} y vertical center
  6816. * @param {Number} r radius
  6817. */
  6818. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  6819. // http://en.wikipedia.org/wiki/Equilateral_triangle
  6820. this.beginPath();
  6821. var s = r * 2;
  6822. var s2 = s / 2;
  6823. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  6824. var h = Math.sqrt(s * s - s2 * s2); // height
  6825. this.moveTo(x, y + (h - ir));
  6826. this.lineTo(x + s2, y - ir);
  6827. this.lineTo(x - s2, y - ir);
  6828. this.lineTo(x, y + (h - ir));
  6829. this.closePath();
  6830. };
  6831. /**
  6832. * Draw a star shape, a star with 5 points
  6833. * @param {Number} x horizontal center
  6834. * @param {Number} y vertical center
  6835. * @param {Number} r radius, half the length of the sides of the triangle
  6836. */
  6837. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  6838. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  6839. this.beginPath();
  6840. for (var n = 0; n < 10; n++) {
  6841. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  6842. this.lineTo(
  6843. x + radius * Math.sin(n * 2 * Math.PI / 10),
  6844. y - radius * Math.cos(n * 2 * Math.PI / 10)
  6845. );
  6846. }
  6847. this.closePath();
  6848. };
  6849. /**
  6850. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  6851. */
  6852. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  6853. var r2d = Math.PI/180;
  6854. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  6855. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  6856. this.beginPath();
  6857. this.moveTo(x+r,y);
  6858. this.lineTo(x+w-r,y);
  6859. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  6860. this.lineTo(x+w,y+h-r);
  6861. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  6862. this.lineTo(x+r,y+h);
  6863. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  6864. this.lineTo(x,y+r);
  6865. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  6866. };
  6867. /**
  6868. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  6869. */
  6870. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  6871. var kappa = .5522848,
  6872. ox = (w / 2) * kappa, // control point offset horizontal
  6873. oy = (h / 2) * kappa, // control point offset vertical
  6874. xe = x + w, // x-end
  6875. ye = y + h, // y-end
  6876. xm = x + w / 2, // x-middle
  6877. ym = y + h / 2; // y-middle
  6878. this.beginPath();
  6879. this.moveTo(x, ym);
  6880. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  6881. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  6882. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  6883. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  6884. };
  6885. /**
  6886. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  6887. */
  6888. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  6889. var f = 1/3;
  6890. var wEllipse = w;
  6891. var hEllipse = h * f;
  6892. var kappa = .5522848,
  6893. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  6894. oy = (hEllipse / 2) * kappa, // control point offset vertical
  6895. xe = x + wEllipse, // x-end
  6896. ye = y + hEllipse, // y-end
  6897. xm = x + wEllipse / 2, // x-middle
  6898. ym = y + hEllipse / 2, // y-middle
  6899. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  6900. yeb = y + h; // y-end, bottom ellipse
  6901. this.beginPath();
  6902. this.moveTo(xe, ym);
  6903. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  6904. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  6905. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  6906. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  6907. this.lineTo(xe, ymb);
  6908. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  6909. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  6910. this.lineTo(x, ym);
  6911. };
  6912. /**
  6913. * Draw an arrow point (no line)
  6914. */
  6915. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  6916. // tail
  6917. var xt = x - length * Math.cos(angle);
  6918. var yt = y - length * Math.sin(angle);
  6919. // inner tail
  6920. // TODO: allow to customize different shapes
  6921. var xi = x - length * 0.9 * Math.cos(angle);
  6922. var yi = y - length * 0.9 * Math.sin(angle);
  6923. // left
  6924. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  6925. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  6926. // right
  6927. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  6928. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  6929. this.beginPath();
  6930. this.moveTo(x, y);
  6931. this.lineTo(xl, yl);
  6932. this.lineTo(xi, yi);
  6933. this.lineTo(xr, yr);
  6934. this.closePath();
  6935. };
  6936. /**
  6937. * Sets up the dashedLine functionality for drawing
  6938. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  6939. * @author David Jordan
  6940. * @date 2012-08-08
  6941. */
  6942. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  6943. if (!dashArray) dashArray=[10,5];
  6944. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  6945. var dashCount = dashArray.length;
  6946. this.moveTo(x, y);
  6947. var dx = (x2-x), dy = (y2-y);
  6948. var slope = dy/dx;
  6949. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  6950. var dashIndex=0, draw=true;
  6951. while (distRemaining>=0.1){
  6952. var dashLength = dashArray[dashIndex++%dashCount];
  6953. if (dashLength > distRemaining) dashLength = distRemaining;
  6954. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  6955. if (dx<0) xStep = -xStep;
  6956. x += xStep;
  6957. y += slope*xStep;
  6958. this[draw ? 'lineTo' : 'moveTo'](x,y);
  6959. distRemaining -= dashLength;
  6960. draw = !draw;
  6961. }
  6962. };
  6963. // TODO: add diamond shape
  6964. }
  6965. /**
  6966. * @class Node
  6967. * A node. A node can be connected to other nodes via one or multiple edges.
  6968. * @param {object} properties An object containing properties for the node. All
  6969. * properties are optional, except for the id.
  6970. * {number} id Id of the node. Required
  6971. * {string} label Text label for the node
  6972. * {number} x Horizontal position of the node
  6973. * {number} y Vertical position of the node
  6974. * {string} shape Node shape, available:
  6975. * "database", "circle", "ellipse",
  6976. * "box", "image", "text", "dot",
  6977. * "star", "triangle", "triangleDown",
  6978. * "square"
  6979. * {string} image An image url
  6980. * {string} title An title text, can be HTML
  6981. * {anytype} group A group name or number
  6982. * @param {Graph.Images} imagelist A list with images. Only needed
  6983. * when the node has an image
  6984. * @param {Graph.Groups} grouplist A list with groups. Needed for
  6985. * retrieving group properties
  6986. * @param {Object} constants An object with default values for
  6987. * example for the color
  6988. */
  6989. function Node(properties, imagelist, grouplist, constants) {
  6990. this.selected = false;
  6991. this.edges = []; // all edges connected to this node
  6992. this.group = constants.nodes.group;
  6993. this.fontSize = constants.nodes.fontSize;
  6994. this.fontFace = constants.nodes.fontFace;
  6995. this.fontColor = constants.nodes.fontColor;
  6996. this.color = constants.nodes.color;
  6997. // set defaults for the properties
  6998. this.id = undefined;
  6999. this.shape = constants.nodes.shape;
  7000. this.image = constants.nodes.image;
  7001. this.x = 0;
  7002. this.y = 0;
  7003. this.xFixed = false;
  7004. this.yFixed = false;
  7005. this.radius = constants.nodes.radius;
  7006. this.radiusFixed = false;
  7007. this.radiusMin = constants.nodes.radiusMin;
  7008. this.radiusMax = constants.nodes.radiusMax;
  7009. this.imagelist = imagelist;
  7010. this.grouplist = grouplist;
  7011. this.setProperties(properties, constants);
  7012. // mass, force, velocity
  7013. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  7014. this.fx = 0.0; // external force x
  7015. this.fy = 0.0; // external force y
  7016. this.vx = 0.0; // velocity x
  7017. this.vy = 0.0; // velocity y
  7018. this.minForce = constants.minForce;
  7019. this.damping = 0.9; // damping factor
  7020. };
  7021. /**
  7022. * Attach a edge to the node
  7023. * @param {Edge} edge
  7024. */
  7025. Node.prototype.attachEdge = function(edge) {
  7026. if (this.edges.indexOf(edge) == -1) {
  7027. this.edges.push(edge);
  7028. }
  7029. this._updateMass();
  7030. };
  7031. /**
  7032. * Detach a edge from the node
  7033. * @param {Edge} edge
  7034. */
  7035. Node.prototype.detachEdge = function(edge) {
  7036. var index = this.edges.indexOf(edge);
  7037. if (index != -1) {
  7038. this.edges.splice(index, 1);
  7039. }
  7040. this._updateMass();
  7041. };
  7042. /**
  7043. * Update the nodes mass, which is determined by the number of edges connecting
  7044. * to it (more edges -> heavier node).
  7045. * @private
  7046. */
  7047. Node.prototype._updateMass = function() {
  7048. this.mass = 50 + 20 * this.edges.length; // kg
  7049. };
  7050. /**
  7051. * Set or overwrite properties for the node
  7052. * @param {Object} properties an object with properties
  7053. * @param {Object} constants and object with default, global properties
  7054. */
  7055. Node.prototype.setProperties = function(properties, constants) {
  7056. if (!properties) {
  7057. return;
  7058. }
  7059. // basic properties
  7060. if (properties.id != undefined) {this.id = properties.id;}
  7061. if (properties.label != undefined) {this.label = properties.label;}
  7062. if (properties.title != undefined) {this.title = properties.title;}
  7063. if (properties.group != undefined) {this.group = properties.group;}
  7064. if (properties.x != undefined) {this.x = properties.x;}
  7065. if (properties.y != undefined) {this.y = properties.y;}
  7066. if (properties.value != undefined) {this.value = properties.value;}
  7067. if (this.id === undefined) {
  7068. throw "Node must have an id";
  7069. }
  7070. // copy group properties
  7071. if (this.group) {
  7072. var groupObj = this.grouplist.get(this.group);
  7073. for (var prop in groupObj) {
  7074. if (groupObj.hasOwnProperty(prop)) {
  7075. this[prop] = groupObj[prop];
  7076. }
  7077. }
  7078. }
  7079. // individual shape properties
  7080. if (properties.shape != undefined) {this.shape = properties.shape;}
  7081. if (properties.image != undefined) {this.image = properties.image;}
  7082. if (properties.radius != undefined) {this.radius = properties.radius;}
  7083. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  7084. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7085. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7086. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7087. if (this.image != undefined) {
  7088. if (this.imagelist) {
  7089. this.imageObj = this.imagelist.load(this.image);
  7090. }
  7091. else {
  7092. throw "No imagelist provided";
  7093. }
  7094. }
  7095. this.xFixed = this.xFixed || (properties.x != undefined);
  7096. this.yFixed = this.yFixed || (properties.y != undefined);
  7097. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  7098. if (this.shape == 'image') {
  7099. this.radiusMin = constants.nodes.widthMin;
  7100. this.radiusMax = constants.nodes.widthMax;
  7101. }
  7102. // choose draw method depending on the shape
  7103. switch (this.shape) {
  7104. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7105. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7106. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7107. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7108. // TODO: add diamond shape
  7109. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7110. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7111. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7112. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7113. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7114. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7115. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7116. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7117. }
  7118. // reset the size of the node, this can be changed
  7119. this._reset();
  7120. };
  7121. /**
  7122. * Parse a color property into an object with border, background, and
  7123. * hightlight colors
  7124. * @param {Object | String} color
  7125. * @return {Object} colorObject
  7126. */
  7127. Node.parseColor = function(color) {
  7128. var c;
  7129. if (util.isString(color)) {
  7130. c = {
  7131. border: color,
  7132. background: color,
  7133. highlight: {
  7134. border: color,
  7135. background: color
  7136. }
  7137. };
  7138. // TODO: automatically generate a nice highlight color
  7139. }
  7140. else {
  7141. c = {};
  7142. c.background = color.background || 'white';
  7143. c.border = color.border || c.background;
  7144. if (util.isString(color.highlight)) {
  7145. c.highlight = {
  7146. border: color.highlight,
  7147. background: color.highlight
  7148. }
  7149. }
  7150. else {
  7151. c.highlight = {};
  7152. c.highlight.background = color.highlight && color.highlight.background || c.background;
  7153. c.highlight.border = color.highlight && color.highlight.border || c.border;
  7154. }
  7155. }
  7156. return c;
  7157. };
  7158. /**
  7159. * select this node
  7160. */
  7161. Node.prototype.select = function() {
  7162. this.selected = true;
  7163. this._reset();
  7164. };
  7165. /**
  7166. * unselect this node
  7167. */
  7168. Node.prototype.unselect = function() {
  7169. this.selected = false;
  7170. this._reset();
  7171. };
  7172. /**
  7173. * Reset the calculated size of the node, forces it to recalculate its size
  7174. * @private
  7175. */
  7176. Node.prototype._reset = function() {
  7177. this.width = undefined;
  7178. this.height = undefined;
  7179. };
  7180. /**
  7181. * get the title of this node.
  7182. * @return {string} title The title of the node, or undefined when no title
  7183. * has been set.
  7184. */
  7185. Node.prototype.getTitle = function() {
  7186. return this.title;
  7187. };
  7188. /**
  7189. * Calculate the distance to the border of the Node
  7190. * @param {CanvasRenderingContext2D} ctx
  7191. * @param {Number} angle Angle in radians
  7192. * @returns {number} distance Distance to the border in pixels
  7193. */
  7194. Node.prototype.distanceToBorder = function (ctx, angle) {
  7195. var borderWidth = 1;
  7196. if (!this.width) {
  7197. this.resize(ctx);
  7198. }
  7199. //noinspection FallthroughInSwitchStatementJS
  7200. switch (this.shape) {
  7201. case 'circle':
  7202. case 'dot':
  7203. return this.radius + borderWidth;
  7204. case 'ellipse':
  7205. var a = this.width / 2;
  7206. var b = this.height / 2;
  7207. var w = (Math.sin(angle) * a);
  7208. var h = (Math.cos(angle) * b);
  7209. return a * b / Math.sqrt(w * w + h * h);
  7210. // TODO: implement distanceToBorder for database
  7211. // TODO: implement distanceToBorder for triangle
  7212. // TODO: implement distanceToBorder for triangleDown
  7213. case 'box':
  7214. case 'image':
  7215. case 'text':
  7216. default:
  7217. if (this.width) {
  7218. return Math.min(
  7219. Math.abs(this.width / 2 / Math.cos(angle)),
  7220. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  7221. // TODO: reckon with border radius too in case of box
  7222. }
  7223. else {
  7224. return 0;
  7225. }
  7226. }
  7227. // TODO: implement calculation of distance to border for all shapes
  7228. };
  7229. /**
  7230. * Set forces acting on the node
  7231. * @param {number} fx Force in horizontal direction
  7232. * @param {number} fy Force in vertical direction
  7233. */
  7234. Node.prototype._setForce = function(fx, fy) {
  7235. this.fx = fx;
  7236. this.fy = fy;
  7237. };
  7238. /**
  7239. * Add forces acting on the node
  7240. * @param {number} fx Force in horizontal direction
  7241. * @param {number} fy Force in vertical direction
  7242. * @private
  7243. */
  7244. Node.prototype._addForce = function(fx, fy) {
  7245. this.fx += fx;
  7246. this.fy += fy;
  7247. };
  7248. /**
  7249. * Perform one discrete step for the node
  7250. * @param {number} interval Time interval in seconds
  7251. */
  7252. Node.prototype.discreteStep = function(interval) {
  7253. if (!this.xFixed) {
  7254. var dx = -this.damping * this.vx; // damping force
  7255. var ax = (this.fx + dx) / this.mass; // acceleration
  7256. this.vx += ax / interval; // velocity
  7257. this.x += this.vx / interval; // position
  7258. }
  7259. if (!this.yFixed) {
  7260. var dy = -this.damping * this.vy; // damping force
  7261. var ay = (this.fy + dy) / this.mass; // acceleration
  7262. this.vy += ay / interval; // velocity
  7263. this.y += this.vy / interval; // position
  7264. }
  7265. };
  7266. /**
  7267. * Check if this node has a fixed x and y position
  7268. * @return {boolean} true if fixed, false if not
  7269. */
  7270. Node.prototype.isFixed = function() {
  7271. return (this.xFixed && this.yFixed);
  7272. };
  7273. /**
  7274. * Check if this node is moving
  7275. * @param {number} vmin the minimum velocity considered as "moving"
  7276. * @return {boolean} true if moving, false if it has no velocity
  7277. */
  7278. // TODO: replace this method with calculating the kinetic energy
  7279. Node.prototype.isMoving = function(vmin) {
  7280. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  7281. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  7282. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  7283. };
  7284. /**
  7285. * check if this node is selecte
  7286. * @return {boolean} selected True if node is selected, else false
  7287. */
  7288. Node.prototype.isSelected = function() {
  7289. return this.selected;
  7290. };
  7291. /**
  7292. * Retrieve the value of the node. Can be undefined
  7293. * @return {Number} value
  7294. */
  7295. Node.prototype.getValue = function() {
  7296. return this.value;
  7297. };
  7298. /**
  7299. * Calculate the distance from the nodes location to the given location (x,y)
  7300. * @param {Number} x
  7301. * @param {Number} y
  7302. * @return {Number} value
  7303. */
  7304. Node.prototype.getDistance = function(x, y) {
  7305. var dx = this.x - x,
  7306. dy = this.y - y;
  7307. return Math.sqrt(dx * dx + dy * dy);
  7308. };
  7309. /**
  7310. * Adjust the value range of the node. The node will adjust it's radius
  7311. * based on its value.
  7312. * @param {Number} min
  7313. * @param {Number} max
  7314. */
  7315. Node.prototype.setValueRange = function(min, max) {
  7316. if (!this.radiusFixed && this.value !== undefined) {
  7317. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  7318. this.radius = (this.value - min) * scale + this.radiusMin;
  7319. }
  7320. };
  7321. /**
  7322. * Draw this node in the given canvas
  7323. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7324. * @param {CanvasRenderingContext2D} ctx
  7325. */
  7326. Node.prototype.draw = function(ctx) {
  7327. throw "Draw method not initialized for node";
  7328. };
  7329. /**
  7330. * Recalculate the size of this node in the given canvas
  7331. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7332. * @param {CanvasRenderingContext2D} ctx
  7333. */
  7334. Node.prototype.resize = function(ctx) {
  7335. throw "Resize method not initialized for node";
  7336. };
  7337. /**
  7338. * Check if this object is overlapping with the provided object
  7339. * @param {Object} obj an object with parameters left, top, right, bottom
  7340. * @return {boolean} True if location is located on node
  7341. */
  7342. Node.prototype.isOverlappingWith = function(obj) {
  7343. return (this.left < obj.right &&
  7344. this.left + this.width > obj.left &&
  7345. this.top < obj.bottom &&
  7346. this.top + this.height > obj.top);
  7347. };
  7348. Node.prototype._resizeImage = function (ctx) {
  7349. // TODO: pre calculate the image size
  7350. if (!this.width) { // undefined or 0
  7351. var width, height;
  7352. if (this.value) {
  7353. var scale = this.imageObj.height / this.imageObj.width;
  7354. width = this.radius || this.imageObj.width;
  7355. height = this.radius * scale || this.imageObj.height;
  7356. }
  7357. else {
  7358. width = this.imageObj.width;
  7359. height = this.imageObj.height;
  7360. }
  7361. this.width = width;
  7362. this.height = height;
  7363. }
  7364. };
  7365. Node.prototype._drawImage = function (ctx) {
  7366. this._resizeImage(ctx);
  7367. this.left = this.x - this.width / 2;
  7368. this.top = this.y - this.height / 2;
  7369. var yLabel;
  7370. if (this.imageObj) {
  7371. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  7372. yLabel = this.y + this.height / 2;
  7373. }
  7374. else {
  7375. // image still loading... just draw the label for now
  7376. yLabel = this.y;
  7377. }
  7378. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  7379. };
  7380. Node.prototype._resizeBox = function (ctx) {
  7381. if (!this.width) {
  7382. var margin = 5;
  7383. var textSize = this.getTextSize(ctx);
  7384. this.width = textSize.width + 2 * margin;
  7385. this.height = textSize.height + 2 * margin;
  7386. }
  7387. };
  7388. Node.prototype._drawBox = function (ctx) {
  7389. this._resizeBox(ctx);
  7390. this.left = this.x - this.width / 2;
  7391. this.top = this.y - this.height / 2;
  7392. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7393. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7394. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7395. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  7396. ctx.fill();
  7397. ctx.stroke();
  7398. this._label(ctx, this.label, this.x, this.y);
  7399. };
  7400. Node.prototype._resizeDatabase = function (ctx) {
  7401. if (!this.width) {
  7402. var margin = 5;
  7403. var textSize = this.getTextSize(ctx);
  7404. var size = textSize.width + 2 * margin;
  7405. this.width = size;
  7406. this.height = size;
  7407. }
  7408. };
  7409. Node.prototype._drawDatabase = function (ctx) {
  7410. this._resizeDatabase(ctx);
  7411. this.left = this.x - this.width / 2;
  7412. this.top = this.y - this.height / 2;
  7413. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7414. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7415. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7416. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  7417. ctx.fill();
  7418. ctx.stroke();
  7419. this._label(ctx, this.label, this.x, this.y);
  7420. };
  7421. Node.prototype._resizeCircle = function (ctx) {
  7422. if (!this.width) {
  7423. var margin = 5;
  7424. var textSize = this.getTextSize(ctx);
  7425. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  7426. this.radius = diameter / 2;
  7427. this.width = diameter;
  7428. this.height = diameter;
  7429. }
  7430. };
  7431. Node.prototype._drawCircle = function (ctx) {
  7432. this._resizeCircle(ctx);
  7433. this.left = this.x - this.width / 2;
  7434. this.top = this.y - this.height / 2;
  7435. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7436. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7437. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7438. ctx.circle(this.x, this.y, this.radius);
  7439. ctx.fill();
  7440. ctx.stroke();
  7441. this._label(ctx, this.label, this.x, this.y);
  7442. };
  7443. Node.prototype._resizeEllipse = function (ctx) {
  7444. if (!this.width) {
  7445. var textSize = this.getTextSize(ctx);
  7446. this.width = textSize.width * 1.5;
  7447. this.height = textSize.height * 2;
  7448. if (this.width < this.height) {
  7449. this.width = this.height;
  7450. }
  7451. }
  7452. };
  7453. Node.prototype._drawEllipse = function (ctx) {
  7454. this._resizeEllipse(ctx);
  7455. this.left = this.x - this.width / 2;
  7456. this.top = this.y - this.height / 2;
  7457. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7458. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7459. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7460. ctx.ellipse(this.left, this.top, this.width, this.height);
  7461. ctx.fill();
  7462. ctx.stroke();
  7463. this._label(ctx, this.label, this.x, this.y);
  7464. };
  7465. Node.prototype._drawDot = function (ctx) {
  7466. this._drawShape(ctx, 'circle');
  7467. };
  7468. Node.prototype._drawTriangle = function (ctx) {
  7469. this._drawShape(ctx, 'triangle');
  7470. };
  7471. Node.prototype._drawTriangleDown = function (ctx) {
  7472. this._drawShape(ctx, 'triangleDown');
  7473. };
  7474. Node.prototype._drawSquare = function (ctx) {
  7475. this._drawShape(ctx, 'square');
  7476. };
  7477. Node.prototype._drawStar = function (ctx) {
  7478. this._drawShape(ctx, 'star');
  7479. };
  7480. Node.prototype._resizeShape = function (ctx) {
  7481. if (!this.width) {
  7482. var size = 2 * this.radius;
  7483. this.width = size;
  7484. this.height = size;
  7485. }
  7486. };
  7487. Node.prototype._drawShape = function (ctx, shape) {
  7488. this._resizeShape(ctx);
  7489. this.left = this.x - this.width / 2;
  7490. this.top = this.y - this.height / 2;
  7491. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7492. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7493. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7494. ctx[shape](this.x, this.y, this.radius);
  7495. ctx.fill();
  7496. ctx.stroke();
  7497. if (this.label) {
  7498. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  7499. }
  7500. };
  7501. Node.prototype._resizeText = function (ctx) {
  7502. if (!this.width) {
  7503. var margin = 5;
  7504. var textSize = this.getTextSize(ctx);
  7505. this.width = textSize.width + 2 * margin;
  7506. this.height = textSize.height + 2 * margin;
  7507. }
  7508. };
  7509. Node.prototype._drawText = function (ctx) {
  7510. this._resizeText(ctx);
  7511. this.left = this.x - this.width / 2;
  7512. this.top = this.y - this.height / 2;
  7513. this._label(ctx, this.label, this.x, this.y);
  7514. };
  7515. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  7516. if (text) {
  7517. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  7518. ctx.fillStyle = this.fontColor || "black";
  7519. ctx.textAlign = align || "center";
  7520. ctx.textBaseline = baseline || "middle";
  7521. var lines = text.split('\n'),
  7522. lineCount = lines.length,
  7523. fontSize = (this.fontSize + 4),
  7524. yLine = y + (1 - lineCount) / 2 * fontSize;
  7525. for (var i = 0; i < lineCount; i++) {
  7526. ctx.fillText(lines[i], x, yLine);
  7527. yLine += fontSize;
  7528. }
  7529. }
  7530. };
  7531. Node.prototype.getTextSize = function(ctx) {
  7532. if (this.label != undefined) {
  7533. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  7534. var lines = this.label.split('\n'),
  7535. height = (this.fontSize + 4) * lines.length,
  7536. width = 0;
  7537. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  7538. width = Math.max(width, ctx.measureText(lines[i]).width);
  7539. }
  7540. return {"width": width, "height": height};
  7541. }
  7542. else {
  7543. return {"width": 0, "height": 0};
  7544. }
  7545. };
  7546. /**
  7547. * @class Edge
  7548. *
  7549. * A edge connects two nodes
  7550. * @param {Object} properties Object with properties. Must contain
  7551. * At least properties from and to.
  7552. * Available properties: from (number),
  7553. * to (number), label (string, color (string),
  7554. * width (number), style (string),
  7555. * length (number), title (string)
  7556. * @param {Graph} graph A graph object, used to find and edge to
  7557. * nodes.
  7558. * @param {Object} constants An object with default values for
  7559. * example for the color
  7560. */
  7561. function Edge (properties, graph, constants) {
  7562. if (!graph) {
  7563. throw "No graph provided";
  7564. }
  7565. this.graph = graph;
  7566. // initialize constants
  7567. this.widthMin = constants.edges.widthMin;
  7568. this.widthMax = constants.edges.widthMax;
  7569. // initialize variables
  7570. this.id = undefined;
  7571. this.fromId = undefined;
  7572. this.toId = undefined;
  7573. this.style = constants.edges.style;
  7574. this.title = undefined;
  7575. this.width = constants.edges.width;
  7576. this.value = undefined;
  7577. this.length = constants.edges.length;
  7578. this.from = null; // a node
  7579. this.to = null; // a node
  7580. this.connected = false;
  7581. // Added to support dashed lines
  7582. // David Jordan
  7583. // 2012-08-08
  7584. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  7585. this.stiffness = undefined; // depends on the length of the edge
  7586. this.color = constants.edges.color;
  7587. this.widthFixed = false;
  7588. this.lengthFixed = false;
  7589. this.setProperties(properties, constants);
  7590. }
  7591. /**
  7592. * Set or overwrite properties for the edge
  7593. * @param {Object} properties an object with properties
  7594. * @param {Object} constants and object with default, global properties
  7595. */
  7596. Edge.prototype.setProperties = function(properties, constants) {
  7597. if (!properties) {
  7598. return;
  7599. }
  7600. if (properties.from != undefined) {this.fromId = properties.from;}
  7601. if (properties.to != undefined) {this.toId = properties.to;}
  7602. if (properties.id != undefined) {this.id = properties.id;}
  7603. if (properties.style != undefined) {this.style = properties.style;}
  7604. if (properties.label != undefined) {this.label = properties.label;}
  7605. if (this.label) {
  7606. this.fontSize = constants.edges.fontSize;
  7607. this.fontFace = constants.edges.fontFace;
  7608. this.fontColor = constants.edges.fontColor;
  7609. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7610. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7611. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7612. }
  7613. if (properties.title != undefined) {this.title = properties.title;}
  7614. if (properties.width != undefined) {this.width = properties.width;}
  7615. if (properties.value != undefined) {this.value = properties.value;}
  7616. if (properties.length != undefined) {this.length = properties.length;}
  7617. // Added to support dashed lines
  7618. // David Jordan
  7619. // 2012-08-08
  7620. if (properties.dash) {
  7621. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  7622. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  7623. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  7624. }
  7625. if (properties.color != undefined) {this.color = properties.color;}
  7626. // A node is connected when it has a from and to node.
  7627. this.connect();
  7628. this.widthFixed = this.widthFixed || (properties.width != undefined);
  7629. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  7630. this.stiffness = 1 / this.length;
  7631. // set draw method based on style
  7632. switch (this.style) {
  7633. case 'line': this.draw = this._drawLine; break;
  7634. case 'arrow': this.draw = this._drawArrow; break;
  7635. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  7636. case 'dash-line': this.draw = this._drawDashLine; break;
  7637. default: this.draw = this._drawLine; break;
  7638. }
  7639. };
  7640. /**
  7641. * Connect an edge to its nodes
  7642. */
  7643. Edge.prototype.connect = function () {
  7644. this.disconnect();
  7645. this.from = this.graph.nodes[this.fromId] || null;
  7646. this.to = this.graph.nodes[this.toId] || null;
  7647. this.connected = (this.from && this.to);
  7648. if (this.connected) {
  7649. this.from.attachEdge(this);
  7650. this.to.attachEdge(this);
  7651. }
  7652. else {
  7653. if (this.from) {
  7654. this.from.detachEdge(this);
  7655. }
  7656. if (this.to) {
  7657. this.to.detachEdge(this);
  7658. }
  7659. }
  7660. };
  7661. /**
  7662. * Disconnect an edge from its nodes
  7663. */
  7664. Edge.prototype.disconnect = function () {
  7665. if (this.from) {
  7666. this.from.detachEdge(this);
  7667. this.from = null;
  7668. }
  7669. if (this.to) {
  7670. this.to.detachEdge(this);
  7671. this.to = null;
  7672. }
  7673. this.connected = false;
  7674. };
  7675. /**
  7676. * get the title of this edge.
  7677. * @return {string} title The title of the edge, or undefined when no title
  7678. * has been set.
  7679. */
  7680. Edge.prototype.getTitle = function() {
  7681. return this.title;
  7682. };
  7683. /**
  7684. * Retrieve the value of the edge. Can be undefined
  7685. * @return {Number} value
  7686. */
  7687. Edge.prototype.getValue = function() {
  7688. return this.value;
  7689. };
  7690. /**
  7691. * Adjust the value range of the edge. The edge will adjust it's width
  7692. * based on its value.
  7693. * @param {Number} min
  7694. * @param {Number} max
  7695. */
  7696. Edge.prototype.setValueRange = function(min, max) {
  7697. if (!this.widthFixed && this.value !== undefined) {
  7698. var factor = (this.widthMax - this.widthMin) / (max - min);
  7699. this.width = (this.value - min) * factor + this.widthMin;
  7700. }
  7701. };
  7702. /**
  7703. * Redraw a edge
  7704. * Draw this edge in the given canvas
  7705. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7706. * @param {CanvasRenderingContext2D} ctx
  7707. */
  7708. Edge.prototype.draw = function(ctx) {
  7709. throw "Method draw not initialized in edge";
  7710. };
  7711. /**
  7712. * Check if this object is overlapping with the provided object
  7713. * @param {Object} obj an object with parameters left, top
  7714. * @return {boolean} True if location is located on the edge
  7715. */
  7716. Edge.prototype.isOverlappingWith = function(obj) {
  7717. var distMax = 10;
  7718. var xFrom = this.from.x;
  7719. var yFrom = this.from.y;
  7720. var xTo = this.to.x;
  7721. var yTo = this.to.y;
  7722. var xObj = obj.left;
  7723. var yObj = obj.top;
  7724. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  7725. return (dist < distMax);
  7726. };
  7727. /**
  7728. * Redraw a edge as a line
  7729. * Draw this edge in the given canvas
  7730. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7731. * @param {CanvasRenderingContext2D} ctx
  7732. * @private
  7733. */
  7734. Edge.prototype._drawLine = function(ctx) {
  7735. // set style
  7736. ctx.strokeStyle = this.color;
  7737. ctx.lineWidth = this._getLineWidth();
  7738. var point;
  7739. if (this.from != this.to) {
  7740. // draw line
  7741. this._line(ctx);
  7742. // draw label
  7743. if (this.label) {
  7744. point = this._pointOnLine(0.5);
  7745. this._label(ctx, this.label, point.x, point.y);
  7746. }
  7747. }
  7748. else {
  7749. var x, y;
  7750. var radius = this.length / 4;
  7751. var node = this.from;
  7752. if (!node.width) {
  7753. node.resize(ctx);
  7754. }
  7755. if (node.width > node.height) {
  7756. x = node.x + node.width / 2;
  7757. y = node.y - radius;
  7758. }
  7759. else {
  7760. x = node.x + radius;
  7761. y = node.y - node.height / 2;
  7762. }
  7763. this._circle(ctx, x, y, radius);
  7764. point = this._pointOnCircle(x, y, radius, 0.5);
  7765. this._label(ctx, this.label, point.x, point.y);
  7766. }
  7767. };
  7768. /**
  7769. * Get the line width of the edge. Depends on width and whether one of the
  7770. * connected nodes is selected.
  7771. * @return {Number} width
  7772. * @private
  7773. */
  7774. Edge.prototype._getLineWidth = function() {
  7775. if (this.from.selected || this.to.selected) {
  7776. return Math.min(this.width * 2, this.widthMax);
  7777. }
  7778. else {
  7779. return this.width;
  7780. }
  7781. };
  7782. /**
  7783. * Draw a line between two nodes
  7784. * @param {CanvasRenderingContext2D} ctx
  7785. * @private
  7786. */
  7787. Edge.prototype._line = function (ctx) {
  7788. // draw a straight line
  7789. ctx.beginPath();
  7790. ctx.moveTo(this.from.x, this.from.y);
  7791. ctx.lineTo(this.to.x, this.to.y);
  7792. ctx.stroke();
  7793. };
  7794. /**
  7795. * Draw a line from a node to itself, a circle
  7796. * @param {CanvasRenderingContext2D} ctx
  7797. * @param {Number} x
  7798. * @param {Number} y
  7799. * @param {Number} radius
  7800. * @private
  7801. */
  7802. Edge.prototype._circle = function (ctx, x, y, radius) {
  7803. // draw a circle
  7804. ctx.beginPath();
  7805. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  7806. ctx.stroke();
  7807. };
  7808. /**
  7809. * Draw label with white background and with the middle at (x, y)
  7810. * @param {CanvasRenderingContext2D} ctx
  7811. * @param {String} text
  7812. * @param {Number} x
  7813. * @param {Number} y
  7814. * @private
  7815. */
  7816. Edge.prototype._label = function (ctx, text, x, y) {
  7817. if (text) {
  7818. // TODO: cache the calculated size
  7819. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  7820. this.fontSize + "px " + this.fontFace;
  7821. ctx.fillStyle = 'white';
  7822. var width = ctx.measureText(text).width;
  7823. var height = this.fontSize;
  7824. var left = x - width / 2;
  7825. var top = y - height / 2;
  7826. ctx.fillRect(left, top, width, height);
  7827. // draw text
  7828. ctx.fillStyle = this.fontColor || "black";
  7829. ctx.textAlign = "left";
  7830. ctx.textBaseline = "top";
  7831. ctx.fillText(text, left, top);
  7832. }
  7833. };
  7834. /**
  7835. * Redraw a edge as a dashed line
  7836. * Draw this edge in the given canvas
  7837. * @author David Jordan
  7838. * @date 2012-08-08
  7839. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7840. * @param {CanvasRenderingContext2D} ctx
  7841. * @private
  7842. */
  7843. Edge.prototype._drawDashLine = function(ctx) {
  7844. // set style
  7845. ctx.strokeStyle = this.color;
  7846. ctx.lineWidth = this._getLineWidth();
  7847. // draw dashed line
  7848. ctx.beginPath();
  7849. ctx.lineCap = 'round';
  7850. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  7851. {
  7852. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  7853. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  7854. }
  7855. 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
  7856. {
  7857. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  7858. [this.dash.length,this.dash.gap]);
  7859. }
  7860. else //If all else fails draw a line
  7861. {
  7862. ctx.moveTo(this.from.x, this.from.y);
  7863. ctx.lineTo(this.to.x, this.to.y);
  7864. }
  7865. ctx.stroke();
  7866. // draw label
  7867. if (this.label) {
  7868. var point = this._pointOnLine(0.5);
  7869. this._label(ctx, this.label, point.x, point.y);
  7870. }
  7871. };
  7872. /**
  7873. * Get a point on a line
  7874. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  7875. * @return {Object} point
  7876. * @private
  7877. */
  7878. Edge.prototype._pointOnLine = function (percentage) {
  7879. return {
  7880. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  7881. y: (1 - percentage) * this.from.y + percentage * this.to.y
  7882. }
  7883. };
  7884. /**
  7885. * Get a point on a circle
  7886. * @param {Number} x
  7887. * @param {Number} y
  7888. * @param {Number} radius
  7889. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  7890. * @return {Object} point
  7891. * @private
  7892. */
  7893. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  7894. var angle = (percentage - 3/8) * 2 * Math.PI;
  7895. return {
  7896. x: x + radius * Math.cos(angle),
  7897. y: y - radius * Math.sin(angle)
  7898. }
  7899. };
  7900. /**
  7901. * Redraw a edge as a line with an arrow halfway the line
  7902. * Draw this edge in the given canvas
  7903. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7904. * @param {CanvasRenderingContext2D} ctx
  7905. * @private
  7906. */
  7907. Edge.prototype._drawArrowCenter = function(ctx) {
  7908. var point;
  7909. // set style
  7910. ctx.strokeStyle = this.color;
  7911. ctx.fillStyle = this.color;
  7912. ctx.lineWidth = this._getLineWidth();
  7913. if (this.from != this.to) {
  7914. // draw line
  7915. this._line(ctx);
  7916. // draw an arrow halfway the line
  7917. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  7918. var length = 10 + 5 * this.width; // TODO: make customizable?
  7919. point = this._pointOnLine(0.5);
  7920. ctx.arrow(point.x, point.y, angle, length);
  7921. ctx.fill();
  7922. ctx.stroke();
  7923. // draw label
  7924. if (this.label) {
  7925. point = this._pointOnLine(0.5);
  7926. this._label(ctx, this.label, point.x, point.y);
  7927. }
  7928. }
  7929. else {
  7930. // draw circle
  7931. var x, y;
  7932. var radius = this.length / 4;
  7933. var node = this.from;
  7934. if (!node.width) {
  7935. node.resize(ctx);
  7936. }
  7937. if (node.width > node.height) {
  7938. x = node.x + node.width / 2;
  7939. y = node.y - radius;
  7940. }
  7941. else {
  7942. x = node.x + radius;
  7943. y = node.y - node.height / 2;
  7944. }
  7945. this._circle(ctx, x, y, radius);
  7946. // draw all arrows
  7947. var angle = 0.2 * Math.PI;
  7948. var length = 10 + 5 * this.width; // TODO: make customizable?
  7949. point = this._pointOnCircle(x, y, radius, 0.5);
  7950. ctx.arrow(point.x, point.y, angle, length);
  7951. ctx.fill();
  7952. ctx.stroke();
  7953. // draw label
  7954. if (this.label) {
  7955. point = this._pointOnCircle(x, y, radius, 0.5);
  7956. this._label(ctx, this.label, point.x, point.y);
  7957. }
  7958. }
  7959. };
  7960. /**
  7961. * Redraw a edge as a line with an arrow
  7962. * Draw this edge in the given canvas
  7963. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7964. * @param {CanvasRenderingContext2D} ctx
  7965. * @private
  7966. */
  7967. Edge.prototype._drawArrow = function(ctx) {
  7968. // set style
  7969. ctx.strokeStyle = this.color;
  7970. ctx.fillStyle = this.color;
  7971. ctx.lineWidth = this._getLineWidth();
  7972. // draw line
  7973. var angle, length;
  7974. if (this.from != this.to) {
  7975. // calculate length and angle of the line
  7976. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  7977. var dx = (this.to.x - this.from.x);
  7978. var dy = (this.to.y - this.from.y);
  7979. var lEdge = Math.sqrt(dx * dx + dy * dy);
  7980. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  7981. var pFrom = (lEdge - lFrom) / lEdge;
  7982. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  7983. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  7984. var lTo = this.to.distanceToBorder(ctx, angle);
  7985. var pTo = (lEdge - lTo) / lEdge;
  7986. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  7987. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  7988. ctx.beginPath();
  7989. ctx.moveTo(xFrom, yFrom);
  7990. ctx.lineTo(xTo, yTo);
  7991. ctx.stroke();
  7992. // draw arrow at the end of the line
  7993. length = 10 + 5 * this.width; // TODO: make customizable?
  7994. ctx.arrow(xTo, yTo, angle, length);
  7995. ctx.fill();
  7996. ctx.stroke();
  7997. // draw label
  7998. if (this.label) {
  7999. var point = this._pointOnLine(0.5);
  8000. this._label(ctx, this.label, point.x, point.y);
  8001. }
  8002. }
  8003. else {
  8004. // draw circle
  8005. var node = this.from;
  8006. var x, y, arrow;
  8007. var radius = this.length / 4;
  8008. if (!node.width) {
  8009. node.resize(ctx);
  8010. }
  8011. if (node.width > node.height) {
  8012. x = node.x + node.width / 2;
  8013. y = node.y - radius;
  8014. arrow = {
  8015. x: x,
  8016. y: node.y,
  8017. angle: 0.9 * Math.PI
  8018. };
  8019. }
  8020. else {
  8021. x = node.x + radius;
  8022. y = node.y - node.height / 2;
  8023. arrow = {
  8024. x: node.x,
  8025. y: y,
  8026. angle: 0.6 * Math.PI
  8027. };
  8028. }
  8029. ctx.beginPath();
  8030. // TODO: do not draw a circle, but an arc
  8031. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  8032. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8033. ctx.stroke();
  8034. // draw all arrows
  8035. length = 10 + 5 * this.width; // TODO: make customizable?
  8036. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  8037. ctx.fill();
  8038. ctx.stroke();
  8039. // draw label
  8040. if (this.label) {
  8041. point = this._pointOnCircle(x, y, radius, 0.5);
  8042. this._label(ctx, this.label, point.x, point.y);
  8043. }
  8044. }
  8045. };
  8046. /**
  8047. * Calculate the distance between a point (x3,y3) and a line segment from
  8048. * (x1,y1) to (x2,y2).
  8049. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  8050. * @param {number} x1
  8051. * @param {number} y1
  8052. * @param {number} x2
  8053. * @param {number} y2
  8054. * @param {number} x3
  8055. * @param {number} y3
  8056. * @private
  8057. */
  8058. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  8059. var px = x2-x1,
  8060. py = y2-y1,
  8061. something = px*px + py*py,
  8062. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  8063. if (u > 1) {
  8064. u = 1;
  8065. }
  8066. else if (u < 0) {
  8067. u = 0;
  8068. }
  8069. var x = x1 + u * px,
  8070. y = y1 + u * py,
  8071. dx = x - x3,
  8072. dy = y - y3;
  8073. //# Note: If the actual distance does not matter,
  8074. //# if you only want to compare what this function
  8075. //# returns to other results of this function, you
  8076. //# can just return the squared distance instead
  8077. //# (i.e. remove the sqrt) to gain a little performance
  8078. return Math.sqrt(dx*dx + dy*dy);
  8079. };
  8080. /**
  8081. * Popup is a class to create a popup window with some text
  8082. * @param {Element} container The container object.
  8083. * @param {Number} [x]
  8084. * @param {Number} [y]
  8085. * @param {String} [text]
  8086. */
  8087. function Popup(container, x, y, text) {
  8088. if (container) {
  8089. this.container = container;
  8090. }
  8091. else {
  8092. this.container = document.body;
  8093. }
  8094. this.x = 0;
  8095. this.y = 0;
  8096. this.padding = 5;
  8097. if (x !== undefined && y !== undefined ) {
  8098. this.setPosition(x, y);
  8099. }
  8100. if (text !== undefined) {
  8101. this.setText(text);
  8102. }
  8103. // create the frame
  8104. this.frame = document.createElement("div");
  8105. var style = this.frame.style;
  8106. style.position = "absolute";
  8107. style.visibility = "hidden";
  8108. style.border = "1px solid #666";
  8109. style.color = "black";
  8110. style.padding = this.padding + "px";
  8111. style.backgroundColor = "#FFFFC6";
  8112. style.borderRadius = "3px";
  8113. style.MozBorderRadius = "3px";
  8114. style.WebkitBorderRadius = "3px";
  8115. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  8116. style.whiteSpace = "nowrap";
  8117. this.container.appendChild(this.frame);
  8118. };
  8119. /**
  8120. * @param {number} x Horizontal position of the popup window
  8121. * @param {number} y Vertical position of the popup window
  8122. */
  8123. Popup.prototype.setPosition = function(x, y) {
  8124. this.x = parseInt(x);
  8125. this.y = parseInt(y);
  8126. };
  8127. /**
  8128. * Set the text for the popup window. This can be HTML code
  8129. * @param {string} text
  8130. */
  8131. Popup.prototype.setText = function(text) {
  8132. this.frame.innerHTML = text;
  8133. };
  8134. /**
  8135. * Show the popup window
  8136. * @param {boolean} show Optional. Show or hide the window
  8137. */
  8138. Popup.prototype.show = function (show) {
  8139. if (show === undefined) {
  8140. show = true;
  8141. }
  8142. if (show) {
  8143. var height = this.frame.clientHeight;
  8144. var width = this.frame.clientWidth;
  8145. var maxHeight = this.frame.parentNode.clientHeight;
  8146. var maxWidth = this.frame.parentNode.clientWidth;
  8147. var top = (this.y - height);
  8148. if (top + height + this.padding > maxHeight) {
  8149. top = maxHeight - height - this.padding;
  8150. }
  8151. if (top < this.padding) {
  8152. top = this.padding;
  8153. }
  8154. var left = this.x;
  8155. if (left + width + this.padding > maxWidth) {
  8156. left = maxWidth - width - this.padding;
  8157. }
  8158. if (left < this.padding) {
  8159. left = this.padding;
  8160. }
  8161. this.frame.style.left = left + "px";
  8162. this.frame.style.top = top + "px";
  8163. this.frame.style.visibility = "visible";
  8164. }
  8165. else {
  8166. this.hide();
  8167. }
  8168. };
  8169. /**
  8170. * Hide the popup window
  8171. */
  8172. Popup.prototype.hide = function () {
  8173. this.frame.style.visibility = "hidden";
  8174. };
  8175. /**
  8176. * @class Groups
  8177. * This class can store groups and properties specific for groups.
  8178. */
  8179. Groups = function () {
  8180. this.clear();
  8181. this.defaultIndex = 0;
  8182. };
  8183. /**
  8184. * default constants for group colors
  8185. */
  8186. Groups.DEFAULT = [
  8187. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  8188. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  8189. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  8190. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  8191. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  8192. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  8193. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  8194. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  8195. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  8196. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  8197. ];
  8198. /**
  8199. * Clear all groups
  8200. */
  8201. Groups.prototype.clear = function () {
  8202. this.groups = {};
  8203. this.groups.length = function()
  8204. {
  8205. var i = 0;
  8206. for ( var p in this ) {
  8207. if (this.hasOwnProperty(p)) {
  8208. i++;
  8209. }
  8210. }
  8211. return i;
  8212. }
  8213. };
  8214. /**
  8215. * get group properties of a groupname. If groupname is not found, a new group
  8216. * is added.
  8217. * @param {*} groupname Can be a number, string, Date, etc.
  8218. * @return {Object} group The created group, containing all group properties
  8219. */
  8220. Groups.prototype.get = function (groupname) {
  8221. var group = this.groups[groupname];
  8222. if (group == undefined) {
  8223. // create new group
  8224. var index = this.defaultIndex % Groups.DEFAULT.length;
  8225. this.defaultIndex++;
  8226. group = {};
  8227. group.color = Groups.DEFAULT[index];
  8228. this.groups[groupname] = group;
  8229. }
  8230. return group;
  8231. };
  8232. /**
  8233. * Add a custom group style
  8234. * @param {String} groupname
  8235. * @param {Object} style An object containing borderColor,
  8236. * backgroundColor, etc.
  8237. * @return {Object} group The created group object
  8238. */
  8239. Groups.prototype.add = function (groupname, style) {
  8240. this.groups[groupname] = style;
  8241. if (style.color) {
  8242. style.color = Node.parseColor(style.color);
  8243. }
  8244. return style;
  8245. };
  8246. /**
  8247. * @class Images
  8248. * This class loads images and keeps them stored.
  8249. */
  8250. Images = function () {
  8251. this.images = {};
  8252. this.callback = undefined;
  8253. };
  8254. /**
  8255. * Set an onload callback function. This will be called each time an image
  8256. * is loaded
  8257. * @param {function} callback
  8258. */
  8259. Images.prototype.setOnloadCallback = function(callback) {
  8260. this.callback = callback;
  8261. };
  8262. /**
  8263. *
  8264. * @param {string} url Url of the image
  8265. * @return {Image} img The image object
  8266. */
  8267. Images.prototype.load = function(url) {
  8268. var img = this.images[url];
  8269. if (img == undefined) {
  8270. // create the image
  8271. var images = this;
  8272. img = new Image();
  8273. this.images[url] = img;
  8274. img.onload = function() {
  8275. if (images.callback) {
  8276. images.callback(this);
  8277. }
  8278. };
  8279. img.src = url;
  8280. }
  8281. return img;
  8282. };
  8283. /**
  8284. * @constructor Graph
  8285. * Create a graph visualization, displaying nodes and edges.
  8286. *
  8287. * @param {Element} container The DOM element in which the Graph will
  8288. * be created. Normally a div element.
  8289. * @param {Object} data An object containing parameters
  8290. * {Array} nodes
  8291. * {Array} edges
  8292. * @param {Object} options Options
  8293. */
  8294. function Graph (container, data, options) {
  8295. // create variables and set default values
  8296. this.containerElement = container;
  8297. this.width = '100%';
  8298. this.height = '100%';
  8299. this.refreshRate = 50; // milliseconds
  8300. this.stabilize = true; // stabilize before displaying the graph
  8301. this.selectable = true;
  8302. // set constant values
  8303. this.constants = {
  8304. nodes: {
  8305. radiusMin: 5,
  8306. radiusMax: 20,
  8307. radius: 5,
  8308. distance: 100, // px
  8309. shape: 'ellipse',
  8310. image: undefined,
  8311. widthMin: 16, // px
  8312. widthMax: 64, // px
  8313. fontColor: 'black',
  8314. fontSize: 14, // px
  8315. //fontFace: verdana,
  8316. fontFace: 'arial',
  8317. color: {
  8318. border: '#2B7CE9',
  8319. background: '#97C2FC',
  8320. highlight: {
  8321. border: '#2B7CE9',
  8322. background: '#D2E5FF'
  8323. }
  8324. },
  8325. borderColor: '#2B7CE9',
  8326. backgroundColor: '#97C2FC',
  8327. highlightColor: '#D2E5FF',
  8328. group: undefined
  8329. },
  8330. edges: {
  8331. widthMin: 1,
  8332. widthMax: 15,
  8333. width: 1,
  8334. style: 'line',
  8335. color: '#343434',
  8336. fontColor: '#343434',
  8337. fontSize: 14, // px
  8338. fontFace: 'arial',
  8339. //distance: 100, //px
  8340. length: 100, // px
  8341. dash: {
  8342. length: 10,
  8343. gap: 5,
  8344. altLength: undefined
  8345. }
  8346. },
  8347. minForce: 0.05,
  8348. minVelocity: 0.02, // px/s
  8349. maxIterations: 1000 // maximum number of iteration to stabilize
  8350. };
  8351. var graph = this;
  8352. this.nodes = {}; // object with Node objects
  8353. this.edges = {}; // object with Edge objects
  8354. // TODO: create a counter to keep track on the number of nodes having values
  8355. // TODO: create a counter to keep track on the number of nodes currently moving
  8356. // TODO: create a counter to keep track on the number of edges having values
  8357. this.nodesData = null; // A DataSet or DataView
  8358. this.edgesData = null; // A DataSet or DataView
  8359. // create event listeners used to subscribe on the DataSets of the nodes and edges
  8360. var me = this;
  8361. this.nodesListeners = {
  8362. 'add': function (event, params) {
  8363. me._addNodes(params.items);
  8364. me.start();
  8365. },
  8366. 'update': function (event, params) {
  8367. me._updateNodes(params.items);
  8368. me.start();
  8369. },
  8370. 'remove': function (event, params) {
  8371. me._removeNodes(params.items);
  8372. me.start();
  8373. }
  8374. };
  8375. this.edgesListeners = {
  8376. 'add': function (event, params) {
  8377. me._addEdges(params.items);
  8378. me.start();
  8379. },
  8380. 'update': function (event, params) {
  8381. me._updateEdges(params.items);
  8382. me.start();
  8383. },
  8384. 'remove': function (event, params) {
  8385. me._removeEdges(params.items);
  8386. me.start();
  8387. }
  8388. };
  8389. this.groups = new Groups(); // object with groups
  8390. this.images = new Images(); // object with images
  8391. this.images.setOnloadCallback(function () {
  8392. graph._redraw();
  8393. });
  8394. // properties of the data
  8395. this.moving = false; // True if any of the nodes have an undefined position
  8396. this.selection = [];
  8397. this.timer = undefined;
  8398. // create a frame and canvas
  8399. this._create();
  8400. // apply options
  8401. this.setOptions(options);
  8402. // draw data
  8403. this.setData(data);
  8404. }
  8405. /**
  8406. * Set nodes and edges, and optionally options as well.
  8407. *
  8408. * @param {Object} data Object containing parameters:
  8409. * {Array | DataSet | DataView} [nodes] Array with nodes
  8410. * {Array | DataSet | DataView} [edges] Array with edges
  8411. * {String} [dot] String containing data in DOT format
  8412. * {Options} [options] Object with options
  8413. */
  8414. Graph.prototype.setData = function(data) {
  8415. if (data && data.dot && (data.nodes || data.edges)) {
  8416. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  8417. ' parameter pair "nodes" and "edges", but not both.');
  8418. }
  8419. // set options
  8420. this.setOptions(data && data.options);
  8421. // set all data
  8422. if (data && data.dot) {
  8423. // parse DOT file
  8424. if(data && data.dot) {
  8425. var dotData = vis.util.DOTToGraph(data.dot);
  8426. this.setData(dotData);
  8427. return;
  8428. }
  8429. }
  8430. else {
  8431. this._setNodes(data && data.nodes);
  8432. this._setEdges(data && data.edges);
  8433. }
  8434. // find a stable position or start animating to a stable position
  8435. if (this.stabilize) {
  8436. this._doStabilize();
  8437. }
  8438. this.start();
  8439. };
  8440. /**
  8441. * Set options
  8442. * @param {Object} options
  8443. */
  8444. Graph.prototype.setOptions = function (options) {
  8445. if (options) {
  8446. // retrieve parameter values
  8447. if (options.width != undefined) {this.width = options.width;}
  8448. if (options.height != undefined) {this.height = options.height;}
  8449. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  8450. if (options.selectable != undefined) {this.selectable = options.selectable;}
  8451. // TODO: work out these options and document them
  8452. if (options.edges) {
  8453. for (var prop in options.edges) {
  8454. if (options.edges.hasOwnProperty(prop)) {
  8455. this.constants.edges[prop] = options.edges[prop];
  8456. }
  8457. }
  8458. if (options.edges.length != undefined &&
  8459. options.nodes && options.nodes.distance == undefined) {
  8460. this.constants.edges.length = options.edges.length;
  8461. this.constants.nodes.distance = options.edges.length * 1.25;
  8462. }
  8463. if (!options.edges.fontColor) {
  8464. this.constants.edges.fontColor = options.edges.color;
  8465. }
  8466. // Added to support dashed lines
  8467. // David Jordan
  8468. // 2012-08-08
  8469. if (options.edges.dash) {
  8470. if (options.edges.dash.length != undefined) {
  8471. this.constants.edges.dash.length = options.edges.dash.length;
  8472. }
  8473. if (options.edges.dash.gap != undefined) {
  8474. this.constants.edges.dash.gap = options.edges.dash.gap;
  8475. }
  8476. if (options.edges.dash.altLength != undefined) {
  8477. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  8478. }
  8479. }
  8480. }
  8481. if (options.nodes) {
  8482. for (prop in options.nodes) {
  8483. if (options.nodes.hasOwnProperty(prop)) {
  8484. this.constants.nodes[prop] = options.nodes[prop];
  8485. }
  8486. }
  8487. if (options.nodes.color) {
  8488. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  8489. }
  8490. /*
  8491. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  8492. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  8493. */
  8494. }
  8495. if (options.groups) {
  8496. for (var groupname in options.groups) {
  8497. if (options.groups.hasOwnProperty(groupname)) {
  8498. var group = options.groups[groupname];
  8499. this.groups.add(groupname, group);
  8500. }
  8501. }
  8502. }
  8503. }
  8504. this.setSize(this.width, this.height);
  8505. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  8506. this._setScale(1);
  8507. };
  8508. /**
  8509. * fire an event
  8510. * @param {String} event The name of an event, for example "select"
  8511. * @param {Object} params Optional object with event parameters
  8512. * @private
  8513. */
  8514. Graph.prototype._trigger = function (event, params) {
  8515. events.trigger(this, event, params);
  8516. };
  8517. /**
  8518. * Create the main frame for the Graph.
  8519. * This function is executed once when a Graph object is created. The frame
  8520. * contains a canvas, and this canvas contains all objects like the axis and
  8521. * nodes.
  8522. * @private
  8523. */
  8524. Graph.prototype._create = function () {
  8525. // remove all elements from the container element.
  8526. while (this.containerElement.hasChildNodes()) {
  8527. this.containerElement.removeChild(this.containerElement.firstChild);
  8528. }
  8529. this.frame = document.createElement("div");
  8530. this.frame.className = "graph-frame";
  8531. this.frame.style.position = "relative";
  8532. this.frame.style.overflow = "hidden";
  8533. // create the graph canvas (HTML canvas element)
  8534. this.frame.canvas = document.createElement( "canvas" );
  8535. this.frame.canvas.style.position = "relative";
  8536. this.frame.appendChild(this.frame.canvas);
  8537. if (!this.frame.canvas.getContext) {
  8538. var noCanvas = document.createElement( "DIV" );
  8539. noCanvas.style.color = "red";
  8540. noCanvas.style.fontWeight = "bold" ;
  8541. noCanvas.style.padding = "10px";
  8542. noCanvas.innerHTML = "Error: your browser does not support HTML canvas";
  8543. this.frame.canvas.appendChild(noCanvas);
  8544. }
  8545. // create event listeners
  8546. var me = this;
  8547. var onmousedown = function (event) {me._onMouseDown(event);};
  8548. var onmousemove = function (event) {me._onMouseMoveTitle(event);};
  8549. var onmousewheel = function (event) {me._onMouseWheel(event);};
  8550. var ontouchstart = function (event) {me._onTouchStart(event);};
  8551. vis.util.addEventListener(this.frame.canvas, "mousedown", onmousedown);
  8552. vis.util.addEventListener(this.frame.canvas, "mousemove", onmousemove);
  8553. vis.util.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
  8554. vis.util.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
  8555. // add the frame to the container element
  8556. this.containerElement.appendChild(this.frame);
  8557. };
  8558. /**
  8559. * handle on mouse down event
  8560. * @private
  8561. */
  8562. Graph.prototype._onMouseDown = function (event) {
  8563. event = event || window.event;
  8564. if (!this.selectable) {
  8565. return;
  8566. }
  8567. // check if mouse is still down (may be up when focus is lost for example
  8568. // in an iframe)
  8569. if (this.leftButtonDown) {
  8570. this._onMouseUp(event);
  8571. }
  8572. // only react on left mouse button down
  8573. this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  8574. if (!this.leftButtonDown && !this.touchDown) {
  8575. return;
  8576. }
  8577. // add event listeners to handle moving the contents
  8578. // we store the function onmousemove and onmouseup in the timeline, so we can
  8579. // remove the eventlisteners lateron in the function mouseUp()
  8580. var me = this;
  8581. if (!this.onmousemove) {
  8582. this.onmousemove = function (event) {me._onMouseMove(event);};
  8583. vis.util.addEventListener(document, "mousemove", me.onmousemove);
  8584. }
  8585. if (!this.onmouseup) {
  8586. this.onmouseup = function (event) {me._onMouseUp(event);};
  8587. vis.util.addEventListener(document, "mouseup", me.onmouseup);
  8588. }
  8589. vis.util.preventDefault(event);
  8590. // store the start x and y position of the mouse
  8591. this.startMouseX = event.clientX || event.targetTouches[0].clientX;
  8592. this.startMouseY = event.clientY || event.targetTouches[0].clientY;
  8593. this.startFrameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
  8594. this.startFrameTop = vis.util.getAbsoluteTop(this.frame.canvas);
  8595. this.startTranslation = this._getTranslation();
  8596. this.ctrlKeyDown = event.ctrlKey;
  8597. this.shiftKeyDown = event.shiftKey;
  8598. var obj = {
  8599. left: this._xToCanvas(this.startMouseX - this.startFrameLeft),
  8600. top: this._yToCanvas(this.startMouseY - this.startFrameTop),
  8601. right: this._xToCanvas(this.startMouseX - this.startFrameLeft),
  8602. bottom: this._yToCanvas(this.startMouseY - this.startFrameTop)
  8603. };
  8604. var overlappingNodes = this._getNodesOverlappingWith(obj);
  8605. // if there are overlapping nodes, select the last one, this is the
  8606. // one which is drawn on top of the others
  8607. this.startClickedObj = (overlappingNodes.length > 0) ?
  8608. overlappingNodes[overlappingNodes.length - 1] : undefined;
  8609. if (this.startClickedObj) {
  8610. // move clicked node with the mouse
  8611. // make the clicked node temporarily fixed, and store their original state
  8612. var node = this.nodes[this.startClickedObj];
  8613. this.startClickedObj.xFixed = node.xFixed;
  8614. this.startClickedObj.yFixed = node.yFixed;
  8615. node.xFixed = true;
  8616. node.yFixed = true;
  8617. if (!this.ctrlKeyDown || !node.isSelected()) {
  8618. // select this node
  8619. this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
  8620. }
  8621. else {
  8622. // unselect this node
  8623. this._unselectNodes([this.startClickedObj]);
  8624. }
  8625. if (!this.moving) {
  8626. this._redraw();
  8627. }
  8628. }
  8629. else if (this.shiftKeyDown) {
  8630. // start selection of multiple nodes
  8631. }
  8632. else {
  8633. // start moving the graph
  8634. this.moved = false;
  8635. }
  8636. };
  8637. /**
  8638. * handle on mouse move event
  8639. * @param {Event} event
  8640. * @private
  8641. */
  8642. Graph.prototype._onMouseMove = function (event) {
  8643. event = event || window.event;
  8644. if (!this.selectable) {
  8645. return;
  8646. }
  8647. var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0;
  8648. var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0;
  8649. this.mouseX = mouseX;
  8650. this.mouseY = mouseY;
  8651. if (this.startClickedObj) {
  8652. var node = this.nodes[this.startClickedObj];
  8653. if (!this.startClickedObj.xFixed)
  8654. node.x = this._xToCanvas(mouseX - this.startFrameLeft);
  8655. if (!this.startClickedObj.yFixed)
  8656. node.y = this._yToCanvas(mouseY - this.startFrameTop);
  8657. // start animation if not yet running
  8658. if (!this.moving) {
  8659. this.moving = true;
  8660. this.start();
  8661. }
  8662. }
  8663. else if (this.shiftKeyDown) {
  8664. // draw a rect from start mouse location to current mouse location
  8665. if (this.frame.selRect == undefined) {
  8666. this.frame.selRect = document.createElement("DIV");
  8667. this.frame.appendChild(this.frame.selRect);
  8668. this.frame.selRect.style.position = "absolute";
  8669. this.frame.selRect.style.border = "1px dashed red";
  8670. }
  8671. var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
  8672. var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop;
  8673. var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
  8674. var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;
  8675. this.frame.selRect.style.left = left + "px";
  8676. this.frame.selRect.style.top = top + "px";
  8677. this.frame.selRect.style.width = (right - left) + "px";
  8678. this.frame.selRect.style.height = (bottom - top) + "px";
  8679. }
  8680. else {
  8681. // move the graph
  8682. var diffX = mouseX - this.startMouseX;
  8683. var diffY = mouseY - this.startMouseY;
  8684. this._setTranslation(
  8685. this.startTranslation.x + diffX,
  8686. this.startTranslation.y + diffY);
  8687. this._redraw();
  8688. this.moved = true;
  8689. }
  8690. vis.util.preventDefault(event);
  8691. };
  8692. /**
  8693. * handle on mouse up event
  8694. * @param {Event} event
  8695. * @private
  8696. */
  8697. Graph.prototype._onMouseUp = function (event) {
  8698. event = event || window.event;
  8699. if (!this.selectable) {
  8700. return;
  8701. }
  8702. // remove event listeners here, important for Safari
  8703. if (this.onmousemove) {
  8704. vis.util.removeEventListener(document, "mousemove", this.onmousemove);
  8705. this.onmousemove = undefined;
  8706. }
  8707. if (this.onmouseup) {
  8708. vis.util.removeEventListener(document, "mouseup", this.onmouseup);
  8709. this.onmouseup = undefined;
  8710. }
  8711. vis.util.preventDefault(event);
  8712. // check selected nodes
  8713. var endMouseX = event.clientX || this.mouseX || 0;
  8714. var endMouseY = event.clientY || this.mouseY || 0;
  8715. var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;
  8716. if (this.startClickedObj) {
  8717. // restore the original fixed state
  8718. var node = this.nodes[this.startClickedObj];
  8719. node.xFixed = this.startClickedObj.xFixed;
  8720. node.yFixed = this.startClickedObj.yFixed;
  8721. }
  8722. else if (this.shiftKeyDown) {
  8723. // select nodes inside selection area
  8724. var obj = {
  8725. "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
  8726. "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
  8727. "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
  8728. "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
  8729. };
  8730. var overlappingNodes = this._getNodesOverlappingWith(obj);
  8731. this._selectNodes(overlappingNodes, ctrlKey);
  8732. this.redraw();
  8733. // remove the selection rectangle
  8734. if (this.frame.selRect) {
  8735. this.frame.removeChild(this.frame.selRect);
  8736. this.frame.selRect = undefined;
  8737. }
  8738. }
  8739. else {
  8740. if (!this.ctrlKeyDown && !this.moved) {
  8741. // remove selection
  8742. this._unselectNodes();
  8743. this._redraw();
  8744. }
  8745. }
  8746. this.leftButtonDown = false;
  8747. this.ctrlKeyDown = false;
  8748. };
  8749. /**
  8750. * Event handler for mouse wheel event, used to zoom the timeline
  8751. * Code from http://adomas.org/javascript-mouse-wheel/
  8752. * @param {Event} event
  8753. * @private
  8754. */
  8755. Graph.prototype._onMouseWheel = function(event) {
  8756. event = event || window.event;
  8757. var mouseX = event.clientX;
  8758. var mouseY = event.clientY;
  8759. // retrieve delta
  8760. var delta = 0;
  8761. if (event.wheelDelta) { /* IE/Opera. */
  8762. delta = event.wheelDelta/120;
  8763. } else if (event.detail) { /* Mozilla case. */
  8764. // In Mozilla, sign of delta is different than in IE.
  8765. // Also, delta is multiple of 3.
  8766. delta = -event.detail/3;
  8767. }
  8768. // If delta is nonzero, handle it.
  8769. // Basically, delta is now positive if wheel was scrolled up,
  8770. // and negative, if wheel was scrolled down.
  8771. if (delta) {
  8772. // determine zoom factor, and adjust the zoom factor such that zooming in
  8773. // and zooming out correspond wich each other
  8774. var zoom = delta / 10;
  8775. if (delta < 0) {
  8776. zoom = zoom / (1 - zoom);
  8777. }
  8778. var scaleOld = this._getScale();
  8779. var scaleNew = scaleOld * (1 + zoom);
  8780. if (scaleNew < 0.01) {
  8781. scaleNew = 0.01;
  8782. }
  8783. if (scaleNew > 10) {
  8784. scaleNew = 10;
  8785. }
  8786. var frameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
  8787. var frameTop = vis.util.getAbsoluteTop(this.frame.canvas);
  8788. var x = mouseX - frameLeft;
  8789. var y = mouseY - frameTop;
  8790. var translation = this._getTranslation();
  8791. var scaleFrac = scaleNew / scaleOld;
  8792. var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
  8793. var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;
  8794. this._setScale(scaleNew);
  8795. this._setTranslation(tx, ty);
  8796. this._redraw();
  8797. }
  8798. // Prevent default actions caused by mouse wheel.
  8799. // That might be ugly, but we handle scrolls somehow
  8800. // anyway, so don't bother here...
  8801. vis.util.preventDefault(event);
  8802. };
  8803. /**
  8804. * Mouse move handler for checking whether the title moves over a node with a title.
  8805. * @param {Event} event
  8806. * @private
  8807. */
  8808. Graph.prototype._onMouseMoveTitle = function (event) {
  8809. event = event || window.event;
  8810. var startMouseX = event.clientX;
  8811. var startMouseY = event.clientY;
  8812. this.startFrameLeft = this.startFrameLeft || vis.util.getAbsoluteLeft(this.frame.canvas);
  8813. this.startFrameTop = this.startFrameTop || vis.util.getAbsoluteTop(this.frame.canvas);
  8814. var x = startMouseX - this.startFrameLeft;
  8815. var y = startMouseY - this.startFrameTop;
  8816. // check if the previously selected node is still selected
  8817. if (this.popupNode) {
  8818. this._checkHidePopup(x, y);
  8819. }
  8820. // start a timeout that will check if the mouse is positioned above
  8821. // an element
  8822. var me = this;
  8823. var checkShow = function() {
  8824. me._checkShowPopup(x, y);
  8825. };
  8826. if (this.popupTimer) {
  8827. clearInterval(this.popupTimer); // stop any running timer
  8828. }
  8829. if (!this.leftButtonDown) {
  8830. this.popupTimer = setTimeout(checkShow, 300);
  8831. }
  8832. };
  8833. /**
  8834. * Check if there is an element on the given position in the graph
  8835. * (a node or edge). If so, and if this element has a title,
  8836. * show a popup window with its title.
  8837. *
  8838. * @param {number} x
  8839. * @param {number} y
  8840. * @private
  8841. */
  8842. Graph.prototype._checkShowPopup = function (x, y) {
  8843. var obj = {
  8844. "left" : this._xToCanvas(x),
  8845. "top" : this._yToCanvas(y),
  8846. "right" : this._xToCanvas(x),
  8847. "bottom" : this._yToCanvas(y)
  8848. };
  8849. var id;
  8850. var lastPopupNode = this.popupNode;
  8851. if (this.popupNode == undefined) {
  8852. // search the nodes for overlap, select the top one in case of multiple nodes
  8853. var nodes = this.nodes;
  8854. for (id in nodes) {
  8855. if (nodes.hasOwnProperty(id)) {
  8856. var node = nodes[id];
  8857. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  8858. this.popupNode = node;
  8859. break;
  8860. }
  8861. }
  8862. }
  8863. }
  8864. if (this.popupNode == undefined) {
  8865. // search the edges for overlap
  8866. var edges = this.edges;
  8867. for (id in edges) {
  8868. if (edges.hasOwnProperty(id)) {
  8869. var edge = edges[id];
  8870. if (edge.connected && (edge.getTitle() != undefined) &&
  8871. edge.isOverlappingWith(obj)) {
  8872. this.popupNode = edge;
  8873. break;
  8874. }
  8875. }
  8876. }
  8877. }
  8878. if (this.popupNode) {
  8879. // show popup message window
  8880. if (this.popupNode != lastPopupNode) {
  8881. var me = this;
  8882. if (!me.popup) {
  8883. me.popup = new Popup(me.frame);
  8884. }
  8885. // adjust a small offset such that the mouse cursor is located in the
  8886. // bottom left location of the popup, and you can easily move over the
  8887. // popup area
  8888. me.popup.setPosition(x - 3, y - 3);
  8889. me.popup.setText(me.popupNode.getTitle());
  8890. me.popup.show();
  8891. }
  8892. }
  8893. else {
  8894. if (this.popup) {
  8895. this.popup.hide();
  8896. }
  8897. }
  8898. };
  8899. /**
  8900. * Check if the popup must be hided, which is the case when the mouse is no
  8901. * longer hovering on the object
  8902. * @param {number} x
  8903. * @param {number} y
  8904. * @private
  8905. */
  8906. Graph.prototype._checkHidePopup = function (x, y) {
  8907. var obj = {
  8908. "left" : x,
  8909. "top" : y,
  8910. "right" : x,
  8911. "bottom" : y
  8912. };
  8913. if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
  8914. this.popupNode = undefined;
  8915. if (this.popup) {
  8916. this.popup.hide();
  8917. }
  8918. }
  8919. };
  8920. /**
  8921. * Event handler for touchstart event on mobile devices
  8922. * @param {Event} event
  8923. * @private
  8924. */
  8925. Graph.prototype._onTouchStart = function(event) {
  8926. vis.util.preventDefault(event);
  8927. if (this.touchDown) {
  8928. // if already moving, return
  8929. return;
  8930. }
  8931. this.touchDown = true;
  8932. var me = this;
  8933. if (!this.ontouchmove) {
  8934. this.ontouchmove = function (event) {me._onTouchMove(event);};
  8935. vis.util.addEventListener(document, "touchmove", this.ontouchmove);
  8936. }
  8937. if (!this.ontouchend) {
  8938. this.ontouchend = function (event) {me._onTouchEnd(event);};
  8939. vis.util.addEventListener(document, "touchend", this.ontouchend);
  8940. }
  8941. this._onMouseDown(event);
  8942. };
  8943. /**
  8944. * Event handler for touchmove event on mobile devices
  8945. * @param {Event} event
  8946. * @private
  8947. */
  8948. Graph.prototype._onTouchMove = function(event) {
  8949. vis.util.preventDefault(event);
  8950. this._onMouseMove(event);
  8951. };
  8952. /**
  8953. * Event handler for touchend event on mobile devices
  8954. * @param {Event} event
  8955. * @private
  8956. */
  8957. Graph.prototype._onTouchEnd = function(event) {
  8958. vis.util.preventDefault(event);
  8959. this.touchDown = false;
  8960. if (this.ontouchmove) {
  8961. vis.util.removeEventListener(document, "touchmove", this.ontouchmove);
  8962. this.ontouchmove = undefined;
  8963. }
  8964. if (this.ontouchend) {
  8965. vis.util.removeEventListener(document, "touchend", this.ontouchend);
  8966. this.ontouchend = undefined;
  8967. }
  8968. this._onMouseUp(event);
  8969. };
  8970. /**
  8971. * Unselect selected nodes. If no selection array is provided, all nodes
  8972. * are unselected
  8973. * @param {Object[]} selection Array with selection objects, each selection
  8974. * object has a parameter row. Optional
  8975. * @param {Boolean} triggerSelect If true (default), the select event
  8976. * is triggered when nodes are unselected
  8977. * @return {Boolean} changed True if the selection is changed
  8978. * @private
  8979. */
  8980. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  8981. var changed = false;
  8982. var i, iMax, id;
  8983. if (selection) {
  8984. // remove provided selections
  8985. for (i = 0, iMax = selection.length; i < iMax; i++) {
  8986. id = selection[i];
  8987. this.nodes[id].unselect();
  8988. var j = 0;
  8989. while (j < this.selection.length) {
  8990. if (this.selection[j] == id) {
  8991. this.selection.splice(j, 1);
  8992. changed = true;
  8993. }
  8994. else {
  8995. j++;
  8996. }
  8997. }
  8998. }
  8999. }
  9000. else if (this.selection && this.selection.length) {
  9001. // remove all selections
  9002. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9003. id = this.selection[i];
  9004. this.nodes[id].unselect();
  9005. changed = true;
  9006. }
  9007. this.selection = [];
  9008. }
  9009. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  9010. // fire the select event
  9011. this._trigger('select');
  9012. }
  9013. return changed;
  9014. };
  9015. /**
  9016. * select all nodes on given location x, y
  9017. * @param {Array} selection an array with node ids
  9018. * @param {boolean} append If true, the new selection will be appended to the
  9019. * current selection (except for duplicate entries)
  9020. * @return {Boolean} changed True if the selection is changed
  9021. * @private
  9022. */
  9023. Graph.prototype._selectNodes = function(selection, append) {
  9024. var changed = false;
  9025. var i, iMax;
  9026. // TODO: the selectNodes method is a little messy, rework this
  9027. // check if the current selection equals the desired selection
  9028. var selectionAlreadyThere = true;
  9029. if (selection.length != this.selection.length) {
  9030. selectionAlreadyThere = false;
  9031. }
  9032. else {
  9033. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  9034. if (selection[i] != this.selection[i]) {
  9035. selectionAlreadyThere = false;
  9036. break;
  9037. }
  9038. }
  9039. }
  9040. if (selectionAlreadyThere) {
  9041. return changed;
  9042. }
  9043. if (append == undefined || append == false) {
  9044. // first deselect any selected node
  9045. var triggerSelect = false;
  9046. changed = this._unselectNodes(undefined, triggerSelect);
  9047. }
  9048. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9049. // add each of the new selections, but only when they are not duplicate
  9050. var id = selection[i];
  9051. var isDuplicate = (this.selection.indexOf(id) != -1);
  9052. if (!isDuplicate) {
  9053. this.nodes[id].select();
  9054. this.selection.push(id);
  9055. changed = true;
  9056. }
  9057. }
  9058. if (changed) {
  9059. // fire the select event
  9060. this._trigger('select');
  9061. }
  9062. return changed;
  9063. };
  9064. /**
  9065. * retrieve all nodes overlapping with given object
  9066. * @param {Object} obj An object with parameters left, top, right, bottom
  9067. * @return {Object[]} An array with selection objects containing
  9068. * the parameter row.
  9069. * @private
  9070. */
  9071. Graph.prototype._getNodesOverlappingWith = function (obj) {
  9072. var nodes = this.nodes,
  9073. overlappingNodes = [];
  9074. for (var id in nodes) {
  9075. if (nodes.hasOwnProperty(id)) {
  9076. if (nodes[id].isOverlappingWith(obj)) {
  9077. overlappingNodes.push(id);
  9078. }
  9079. }
  9080. }
  9081. return overlappingNodes;
  9082. };
  9083. /**
  9084. * retrieve the currently selected nodes
  9085. * @return {Number[] | String[]} selection An array with the ids of the
  9086. * selected nodes.
  9087. */
  9088. Graph.prototype.getSelection = function() {
  9089. return this.selection.concat([]);
  9090. };
  9091. /**
  9092. * select zero or more nodes
  9093. * @param {Number[] | String[]} selection An array with the ids of the
  9094. * selected nodes.
  9095. */
  9096. Graph.prototype.setSelection = function(selection) {
  9097. var i, iMax, id;
  9098. if (selection.length == undefined)
  9099. throw "Selection must be an array with ids";
  9100. // first unselect any selected node
  9101. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9102. id = this.selection[i];
  9103. this.nodes[id].unselect();
  9104. }
  9105. this.selection = [];
  9106. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9107. id = selection[i];
  9108. var node = this.nodes[id];
  9109. if (!node) {
  9110. throw new RangeError('Node with id "' + id + '" not found');
  9111. }
  9112. node.select();
  9113. this.selection.push(id);
  9114. }
  9115. this.redraw();
  9116. };
  9117. /**
  9118. * Validate the selection: remove ids of nodes which no longer exist
  9119. * @private
  9120. */
  9121. Graph.prototype._updateSelection = function () {
  9122. var i = 0;
  9123. while (i < this.selection.length) {
  9124. var id = this.selection[i];
  9125. if (!this.nodes[id]) {
  9126. this.selection.splice(i, 1);
  9127. }
  9128. else {
  9129. i++;
  9130. }
  9131. }
  9132. };
  9133. /**
  9134. * Temporary method to test calculating a hub value for the nodes
  9135. * @param {number} level Maximum number edges between two nodes in order
  9136. * to call them connected. Optional, 1 by default
  9137. * @return {Number[]} connectioncount array with the connection count
  9138. * for each node
  9139. * @private
  9140. */
  9141. Graph.prototype._getConnectionCount = function(level) {
  9142. if (level == undefined) {
  9143. level = 1;
  9144. }
  9145. // get the nodes connected to given nodes
  9146. function getConnectedNodes(nodes) {
  9147. var connectedNodes = [];
  9148. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  9149. var node = nodes[j];
  9150. // find all nodes connected to this node
  9151. var edges = node.edges;
  9152. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  9153. var edge = edges[i];
  9154. var other = null;
  9155. // check if connected
  9156. if (edge.from == node)
  9157. other = edge.to;
  9158. else if (edge.to == node)
  9159. other = edge.from;
  9160. // check if the other node is not already in the list with nodes
  9161. var k, kMax;
  9162. if (other) {
  9163. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  9164. if (nodes[k] == other) {
  9165. other = null;
  9166. break;
  9167. }
  9168. }
  9169. }
  9170. if (other) {
  9171. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  9172. if (connectedNodes[k] == other) {
  9173. other = null;
  9174. break;
  9175. }
  9176. }
  9177. }
  9178. if (other)
  9179. connectedNodes.push(other);
  9180. }
  9181. }
  9182. return connectedNodes;
  9183. }
  9184. var connections = [];
  9185. var nodes = this.nodes;
  9186. for (var id in nodes) {
  9187. if (nodes.hasOwnProperty(id)) {
  9188. var c = [nodes[id]];
  9189. for (var l = 0; l < level; l++) {
  9190. c = c.concat(getConnectedNodes(c));
  9191. }
  9192. connections.push(c);
  9193. }
  9194. }
  9195. var hubs = [];
  9196. for (var i = 0, len = connections.length; i < len; i++) {
  9197. hubs.push(connections[i].length);
  9198. }
  9199. return hubs;
  9200. };
  9201. /**
  9202. * Set a new size for the graph
  9203. * @param {string} width Width in pixels or percentage (for example "800px"
  9204. * or "50%")
  9205. * @param {string} height Height in pixels or percentage (for example "400px"
  9206. * or "30%")
  9207. */
  9208. Graph.prototype.setSize = function(width, height) {
  9209. this.frame.style.width = width;
  9210. this.frame.style.height = height;
  9211. this.frame.canvas.style.width = "100%";
  9212. this.frame.canvas.style.height = "100%";
  9213. this.frame.canvas.width = this.frame.canvas.clientWidth;
  9214. this.frame.canvas.height = this.frame.canvas.clientHeight;
  9215. };
  9216. /**
  9217. * Set a data set with nodes for the graph
  9218. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  9219. * @private
  9220. */
  9221. Graph.prototype._setNodes = function(nodes) {
  9222. var oldNodesData = this.nodesData;
  9223. if (nodes instanceof DataSet || nodes instanceof DataView) {
  9224. this.nodesData = nodes;
  9225. }
  9226. else if (nodes instanceof Array) {
  9227. this.nodesData = new DataSet();
  9228. this.nodesData.add(nodes);
  9229. }
  9230. else if (!nodes) {
  9231. this.nodesData = new DataSet();
  9232. }
  9233. else {
  9234. throw new TypeError('Array or DataSet expected');
  9235. }
  9236. if (oldNodesData) {
  9237. // unsubscribe from old dataset
  9238. util.forEach(this.nodesListeners, function (callback, event) {
  9239. oldNodesData.unsubscribe(event, callback);
  9240. });
  9241. }
  9242. // remove drawn nodes
  9243. this.nodes = {};
  9244. if (this.nodesData) {
  9245. // subscribe to new dataset
  9246. var me = this;
  9247. util.forEach(this.nodesListeners, function (callback, event) {
  9248. me.nodesData.subscribe(event, callback);
  9249. });
  9250. // draw all new nodes
  9251. var ids = this.nodesData.getIds();
  9252. this._addNodes(ids);
  9253. }
  9254. this._updateSelection();
  9255. };
  9256. /**
  9257. * Add nodes
  9258. * @param {Number[] | String[]} ids
  9259. * @private
  9260. */
  9261. Graph.prototype._addNodes = function(ids) {
  9262. var id;
  9263. for (var i = 0, len = ids.length; i < len; i++) {
  9264. id = ids[i];
  9265. var data = this.nodesData.get(id);
  9266. var node = new Node(data, this.images, this.groups, this.constants);
  9267. this.nodes[id] = node; // note: this may replace an existing node
  9268. if (!node.isFixed()) {
  9269. // TODO: position new nodes in a smarter way!
  9270. var radius = this.constants.edges.length * 2;
  9271. var count = ids.length;
  9272. var angle = 2 * Math.PI * (i / count);
  9273. node.x = radius * Math.cos(angle);
  9274. node.y = radius * Math.sin(angle);
  9275. // note: no not use node.isMoving() here, as that gives the current
  9276. // velocity of the node, which is zero after creation of the node.
  9277. this.moving = true;
  9278. }
  9279. }
  9280. this._reconnectEdges();
  9281. this._updateValueRange(this.nodes);
  9282. };
  9283. /**
  9284. * Update existing nodes, or create them when not yet existing
  9285. * @param {Number[] | String[]} ids
  9286. * @private
  9287. */
  9288. Graph.prototype._updateNodes = function(ids) {
  9289. var nodes = this.nodes,
  9290. nodesData = this.nodesData;
  9291. for (var i = 0, len = ids.length; i < len; i++) {
  9292. var id = ids[i];
  9293. var node = nodes[id];
  9294. var data = nodesData.get(id);
  9295. if (node) {
  9296. // update node
  9297. node.setProperties(data, this.constants);
  9298. }
  9299. else {
  9300. // create node
  9301. node = new Node(properties, this.images, this.groups, this.constants);
  9302. nodes[id] = node;
  9303. if (!node.isFixed()) {
  9304. this.moving = true;
  9305. }
  9306. }
  9307. }
  9308. this._reconnectEdges();
  9309. this._updateValueRange(nodes);
  9310. };
  9311. /**
  9312. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  9313. * @param {Number[] | String[]} ids
  9314. * @private
  9315. */
  9316. Graph.prototype._removeNodes = function(ids) {
  9317. var nodes = this.nodes;
  9318. for (var i = 0, len = ids.length; i < len; i++) {
  9319. var id = ids[i];
  9320. delete nodes[id];
  9321. }
  9322. this._reconnectEdges();
  9323. this._updateSelection();
  9324. this._updateValueRange(nodes);
  9325. };
  9326. /**
  9327. * Load edges by reading the data table
  9328. * @param {Array | DataSet | DataView} edges The data containing the edges.
  9329. * @private
  9330. * @private
  9331. */
  9332. Graph.prototype._setEdges = function(edges) {
  9333. var oldEdgesData = this.edgesData;
  9334. if (edges instanceof DataSet || edges instanceof DataView) {
  9335. this.edgesData = edges;
  9336. }
  9337. else if (edges instanceof Array) {
  9338. this.edgesData = new DataSet();
  9339. this.edgesData.add(edges);
  9340. }
  9341. else if (!edges) {
  9342. this.edgesData = new DataSet();
  9343. }
  9344. else {
  9345. throw new TypeError('Array or DataSet expected');
  9346. }
  9347. if (oldEdgesData) {
  9348. // unsubscribe from old dataset
  9349. util.forEach(this.edgesListeners, function (callback, event) {
  9350. oldEdgesData.unsubscribe(event, callback);
  9351. });
  9352. }
  9353. // remove drawn edges
  9354. this.edges = {};
  9355. if (this.edgesData) {
  9356. // subscribe to new dataset
  9357. var me = this;
  9358. util.forEach(this.edgesListeners, function (callback, event) {
  9359. me.edgesData.subscribe(event, callback);
  9360. });
  9361. // draw all new nodes
  9362. var ids = this.edgesData.getIds();
  9363. this._addEdges(ids);
  9364. }
  9365. this._reconnectEdges();
  9366. };
  9367. /**
  9368. * Add edges
  9369. * @param {Number[] | String[]} ids
  9370. * @private
  9371. */
  9372. Graph.prototype._addEdges = function (ids) {
  9373. var edges = this.edges,
  9374. edgesData = this.edgesData;
  9375. for (var i = 0, len = ids.length; i < len; i++) {
  9376. var id = ids[i];
  9377. var oldEdge = edges[id];
  9378. if (oldEdge) {
  9379. oldEdge.disconnect();
  9380. }
  9381. var data = edgesData.get(id);
  9382. edges[id] = new Edge(data, this, this.constants);
  9383. }
  9384. this.moving = true;
  9385. this._updateValueRange(edges);
  9386. };
  9387. /**
  9388. * Update existing edges, or create them when not yet existing
  9389. * @param {Number[] | String[]} ids
  9390. * @private
  9391. */
  9392. Graph.prototype._updateEdges = function (ids) {
  9393. var edges = this.edges,
  9394. edgesData = this.edgesData;
  9395. for (var i = 0, len = ids.length; i < len; i++) {
  9396. var id = ids[i];
  9397. var data = edgesData.get(id);
  9398. var edge = edges[id];
  9399. if (edge) {
  9400. // update edge
  9401. edge.disconnect();
  9402. edge.setProperties(data, this.constants);
  9403. edge.connect();
  9404. }
  9405. else {
  9406. // create edge
  9407. edge = new Edge(data, this, this.constants);
  9408. this.edges[id] = edge;
  9409. }
  9410. }
  9411. this.moving = true;
  9412. this._updateValueRange(edges);
  9413. };
  9414. /**
  9415. * Remove existing edges. Non existing ids will be ignored
  9416. * @param {Number[] | String[]} ids
  9417. * @private
  9418. */
  9419. Graph.prototype._removeEdges = function (ids) {
  9420. var edges = this.edges;
  9421. for (var i = 0, len = ids.length; i < len; i++) {
  9422. var id = ids[i];
  9423. var edge = edges[id];
  9424. if (edge) {
  9425. edge.disconnect();
  9426. delete edges[id];
  9427. }
  9428. }
  9429. this.moving = true;
  9430. this._updateValueRange(edges);
  9431. };
  9432. /**
  9433. * Reconnect all edges
  9434. * @private
  9435. */
  9436. Graph.prototype._reconnectEdges = function() {
  9437. var id,
  9438. nodes = this.nodes,
  9439. edges = this.edges;
  9440. for (id in nodes) {
  9441. if (nodes.hasOwnProperty(id)) {
  9442. nodes[id].edges = [];
  9443. }
  9444. }
  9445. for (id in edges) {
  9446. if (edges.hasOwnProperty(id)) {
  9447. var edge = edges[id];
  9448. edge.from = null;
  9449. edge.to = null;
  9450. edge.connect();
  9451. }
  9452. }
  9453. };
  9454. /**
  9455. * Update the values of all object in the given array according to the current
  9456. * value range of the objects in the array.
  9457. * @param {Object} obj An object containing a set of Edges or Nodes
  9458. * The objects must have a method getValue() and
  9459. * setValueRange(min, max).
  9460. * @private
  9461. */
  9462. Graph.prototype._updateValueRange = function(obj) {
  9463. var id;
  9464. // determine the range of the objects
  9465. var valueMin = undefined;
  9466. var valueMax = undefined;
  9467. for (id in obj) {
  9468. if (obj.hasOwnProperty(id)) {
  9469. var value = obj[id].getValue();
  9470. if (value !== undefined) {
  9471. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  9472. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  9473. }
  9474. }
  9475. }
  9476. // adjust the range of all objects
  9477. if (valueMin !== undefined && valueMax !== undefined) {
  9478. for (id in obj) {
  9479. if (obj.hasOwnProperty(id)) {
  9480. obj[id].setValueRange(valueMin, valueMax);
  9481. }
  9482. }
  9483. }
  9484. };
  9485. /**
  9486. * Redraw the graph with the current data
  9487. * chart will be resized too.
  9488. */
  9489. Graph.prototype.redraw = function() {
  9490. this.setSize(this.width, this.height);
  9491. this._redraw();
  9492. };
  9493. /**
  9494. * Redraw the graph with the current data
  9495. * @private
  9496. */
  9497. Graph.prototype._redraw = function() {
  9498. var ctx = this.frame.canvas.getContext("2d");
  9499. // clear the canvas
  9500. var w = this.frame.canvas.width;
  9501. var h = this.frame.canvas.height;
  9502. ctx.clearRect(0, 0, w, h);
  9503. // set scaling and translation
  9504. ctx.save();
  9505. ctx.translate(this.translation.x, this.translation.y);
  9506. ctx.scale(this.scale, this.scale);
  9507. this._drawEdges(ctx);
  9508. this._drawNodes(ctx);
  9509. // restore original scaling and translation
  9510. ctx.restore();
  9511. };
  9512. /**
  9513. * Set the translation of the graph
  9514. * @param {Number} offsetX Horizontal offset
  9515. * @param {Number} offsetY Vertical offset
  9516. * @private
  9517. */
  9518. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  9519. if (this.translation === undefined) {
  9520. this.translation = {
  9521. "x": 0,
  9522. "y": 0
  9523. };
  9524. }
  9525. if (offsetX !== undefined) {
  9526. this.translation.x = offsetX;
  9527. }
  9528. if (offsetY !== undefined) {
  9529. this.translation.y = offsetY;
  9530. }
  9531. };
  9532. /**
  9533. * Get the translation of the graph
  9534. * @return {Object} translation An object with parameters x and y, both a number
  9535. * @private
  9536. */
  9537. Graph.prototype._getTranslation = function() {
  9538. return {
  9539. "x": this.translation.x,
  9540. "y": this.translation.y
  9541. };
  9542. };
  9543. /**
  9544. * Scale the graph
  9545. * @param {Number} scale Scaling factor 1.0 is unscaled
  9546. * @private
  9547. */
  9548. Graph.prototype._setScale = function(scale) {
  9549. this.scale = scale;
  9550. };
  9551. /**
  9552. * Get the current scale of the graph
  9553. * @return {Number} scale Scaling factor 1.0 is unscaled
  9554. * @private
  9555. */
  9556. Graph.prototype._getScale = function() {
  9557. return this.scale;
  9558. };
  9559. Graph.prototype._xToCanvas = function(x) {
  9560. return (x - this.translation.x) / this.scale;
  9561. };
  9562. Graph.prototype._canvasToX = function(x) {
  9563. return x * this.scale + this.translation.x;
  9564. };
  9565. Graph.prototype._yToCanvas = function(y) {
  9566. return (y - this.translation.y) / this.scale;
  9567. };
  9568. Graph.prototype._canvasToY = function(y) {
  9569. return y * this.scale + this.translation.y ;
  9570. };
  9571. /**
  9572. * Redraw all nodes
  9573. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9574. * @param {CanvasRenderingContext2D} ctx
  9575. * @private
  9576. */
  9577. Graph.prototype._drawNodes = function(ctx) {
  9578. // first draw the unselected nodes
  9579. var nodes = this.nodes;
  9580. var selected = [];
  9581. for (var id in nodes) {
  9582. if (nodes.hasOwnProperty(id)) {
  9583. if (nodes[id].isSelected()) {
  9584. selected.push(id);
  9585. }
  9586. else {
  9587. nodes[id].draw(ctx);
  9588. }
  9589. }
  9590. }
  9591. // draw the selected nodes on top
  9592. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  9593. nodes[selected[s]].draw(ctx);
  9594. }
  9595. };
  9596. /**
  9597. * Redraw all edges
  9598. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9599. * @param {CanvasRenderingContext2D} ctx
  9600. * @private
  9601. */
  9602. Graph.prototype._drawEdges = function(ctx) {
  9603. var edges = this.edges;
  9604. for (var id in edges) {
  9605. if (edges.hasOwnProperty(id)) {
  9606. var edge = edges[id];
  9607. if (edge.connected) {
  9608. edges[id].draw(ctx);
  9609. }
  9610. }
  9611. }
  9612. };
  9613. /**
  9614. * Find a stable position for all nodes
  9615. * @private
  9616. */
  9617. Graph.prototype._doStabilize = function() {
  9618. var start = new Date();
  9619. // find stable position
  9620. var count = 0;
  9621. var vmin = this.constants.minVelocity;
  9622. var stable = false;
  9623. while (!stable && count < this.constants.maxIterations) {
  9624. this._calculateForces();
  9625. this._discreteStepNodes();
  9626. stable = !this._isMoving(vmin);
  9627. count++;
  9628. }
  9629. var end = new Date();
  9630. // console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
  9631. };
  9632. /**
  9633. * Calculate the external forces acting on the nodes
  9634. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9635. * @private
  9636. */
  9637. Graph.prototype._calculateForces = function() {
  9638. // create a local edge to the nodes and edges, that is faster
  9639. var id, dx, dy, angle, distance, fx, fy,
  9640. repulsingForce, springForce, length, edgeLength,
  9641. nodes = this.nodes,
  9642. edges = this.edges;
  9643. // gravity, add a small constant force to pull the nodes towards the center of
  9644. // the graph
  9645. // Also, the forces are reset to zero in this loop by using _setForce instead
  9646. // of _addForce
  9647. var gravity = 0.01,
  9648. gx = this.frame.canvas.clientWidth / 2,
  9649. gy = this.frame.canvas.clientHeight / 2;
  9650. for (id in nodes) {
  9651. if (nodes.hasOwnProperty(id)) {
  9652. var node = nodes[id];
  9653. dx = gx - node.x;
  9654. dy = gy - node.y;
  9655. angle = Math.atan2(dy, dx);
  9656. fx = Math.cos(angle) * gravity;
  9657. fy = Math.sin(angle) * gravity;
  9658. node._setForce(fx, fy);
  9659. }
  9660. }
  9661. // repulsing forces between nodes
  9662. var minimumDistance = this.constants.nodes.distance,
  9663. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  9664. for (var id1 in nodes) {
  9665. if (nodes.hasOwnProperty(id1)) {
  9666. var node1 = nodes[id1];
  9667. for (var id2 in nodes) {
  9668. if (nodes.hasOwnProperty(id2)) {
  9669. var node2 = nodes[id2];
  9670. // calculate normally distributed force
  9671. dx = node2.x - node1.x;
  9672. dy = node2.y - node1.y;
  9673. distance = Math.sqrt(dx * dx + dy * dy);
  9674. angle = Math.atan2(dy, dx);
  9675. // TODO: correct factor for repulsing force
  9676. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9677. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9678. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  9679. fx = Math.cos(angle) * repulsingForce;
  9680. fy = Math.sin(angle) * repulsingForce;
  9681. node1._addForce(-fx, -fy);
  9682. node2._addForce(fx, fy);
  9683. }
  9684. }
  9685. }
  9686. }
  9687. /* TODO: re-implement repulsion of edges
  9688. for (var n = 0; n < nodes.length; n++) {
  9689. for (var l = 0; l < edges.length; l++) {
  9690. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  9691. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  9692. // calculate normally distributed force
  9693. dx = nodes[n].x - lx,
  9694. dy = nodes[n].y - ly,
  9695. distance = Math.sqrt(dx * dx + dy * dy),
  9696. angle = Math.atan2(dy, dx),
  9697. // TODO: correct factor for repulsing force
  9698. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9699. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9700. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  9701. fx = Math.cos(angle) * repulsingforce,
  9702. fy = Math.sin(angle) * repulsingforce;
  9703. nodes[n]._addForce(fx, fy);
  9704. edges[l].from._addForce(-fx/2,-fy/2);
  9705. edges[l].to._addForce(-fx/2,-fy/2);
  9706. }
  9707. }
  9708. */
  9709. // forces caused by the edges, modelled as springs
  9710. for (id in edges) {
  9711. if (edges.hasOwnProperty(id)) {
  9712. var edge = edges[id];
  9713. if (edge.connected) {
  9714. dx = (edge.to.x - edge.from.x);
  9715. dy = (edge.to.y - edge.from.y);
  9716. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  9717. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  9718. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  9719. edgeLength = edge.length;
  9720. length = Math.sqrt(dx * dx + dy * dy);
  9721. angle = Math.atan2(dy, dx);
  9722. springForce = edge.stiffness * (edgeLength - length);
  9723. fx = Math.cos(angle) * springForce;
  9724. fy = Math.sin(angle) * springForce;
  9725. edge.from._addForce(-fx, -fy);
  9726. edge.to._addForce(fx, fy);
  9727. }
  9728. }
  9729. }
  9730. /* TODO: re-implement repulsion of edges
  9731. // repulsing forces between edges
  9732. var minimumDistance = this.constants.edges.distance,
  9733. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  9734. for (var l = 0; l < edges.length; l++) {
  9735. //Keep distance from other edge centers
  9736. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  9737. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  9738. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  9739. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  9740. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  9741. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  9742. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  9743. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  9744. // calculate normally distributed force
  9745. dx = l2x - lx,
  9746. dy = l2y - ly,
  9747. distance = Math.sqrt(dx * dx + dy * dy),
  9748. angle = Math.atan2(dy, dx),
  9749. // TODO: correct factor for repulsing force
  9750. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9751. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9752. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  9753. fx = Math.cos(angle) * repulsingforce,
  9754. fy = Math.sin(angle) * repulsingforce;
  9755. edges[l].from._addForce(-fx, -fy);
  9756. edges[l].to._addForce(-fx, -fy);
  9757. edges[l2].from._addForce(fx, fy);
  9758. edges[l2].to._addForce(fx, fy);
  9759. }
  9760. }
  9761. */
  9762. };
  9763. /**
  9764. * Check if any of the nodes is still moving
  9765. * @param {number} vmin the minimum velocity considered as "moving"
  9766. * @return {boolean} true if moving, false if non of the nodes is moving
  9767. * @private
  9768. */
  9769. Graph.prototype._isMoving = function(vmin) {
  9770. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  9771. var nodes = this.nodes;
  9772. for (var id in nodes) {
  9773. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  9774. return true;
  9775. }
  9776. }
  9777. return false;
  9778. };
  9779. /**
  9780. * Perform one discrete step for all nodes
  9781. * @private
  9782. */
  9783. Graph.prototype._discreteStepNodes = function() {
  9784. var interval = this.refreshRate / 1000.0; // in seconds
  9785. var nodes = this.nodes;
  9786. for (var id in nodes) {
  9787. if (nodes.hasOwnProperty(id)) {
  9788. nodes[id].discreteStep(interval);
  9789. }
  9790. }
  9791. };
  9792. /**
  9793. * Start animating nodes and edges
  9794. */
  9795. Graph.prototype.start = function() {
  9796. if (this.moving) {
  9797. this._calculateForces();
  9798. this._discreteStepNodes();
  9799. var vmin = this.constants.minVelocity;
  9800. this.moving = this._isMoving(vmin);
  9801. }
  9802. if (this.moving) {
  9803. // start animation. only start timer if it is not already running
  9804. if (!this.timer) {
  9805. var graph = this;
  9806. this.timer = window.setTimeout(function () {
  9807. graph.timer = undefined;
  9808. graph.start();
  9809. graph._redraw();
  9810. }, this.refreshRate);
  9811. }
  9812. }
  9813. else {
  9814. this._redraw();
  9815. }
  9816. };
  9817. /**
  9818. * Stop animating nodes and edges.
  9819. */
  9820. Graph.prototype.stop = function () {
  9821. if (this.timer) {
  9822. window.clearInterval(this.timer);
  9823. this.timer = undefined;
  9824. }
  9825. };
  9826. /**
  9827. * vis.js module exports
  9828. */
  9829. var vis = {
  9830. util: util,
  9831. events: events,
  9832. Controller: Controller,
  9833. DataSet: DataSet,
  9834. DataView: DataView,
  9835. Range: Range,
  9836. Stack: Stack,
  9837. TimeStep: TimeStep,
  9838. EventBus: EventBus,
  9839. components: {
  9840. items: {
  9841. Item: Item,
  9842. ItemBox: ItemBox,
  9843. ItemPoint: ItemPoint,
  9844. ItemRange: ItemRange
  9845. },
  9846. Component: Component,
  9847. Panel: Panel,
  9848. RootPanel: RootPanel,
  9849. ItemSet: ItemSet,
  9850. TimeAxis: TimeAxis
  9851. },
  9852. graph: {
  9853. Node: Node,
  9854. Edge: Edge,
  9855. Popup: Popup,
  9856. Groups: Groups,
  9857. Images: Images
  9858. },
  9859. Timeline: Timeline,
  9860. Graph: Graph
  9861. };
  9862. /**
  9863. * CommonJS module exports
  9864. */
  9865. if (typeof exports !== 'undefined') {
  9866. exports = vis;
  9867. }
  9868. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  9869. module.exports = vis;
  9870. }
  9871. /**
  9872. * AMD module exports
  9873. */
  9874. if (typeof(define) === 'function') {
  9875. define(function () {
  9876. return vis;
  9877. });
  9878. }
  9879. /**
  9880. * Window exports
  9881. */
  9882. if (typeof window !== 'undefined') {
  9883. // attach the module to the window, load as a regular javascript file
  9884. window['vis'] = vis;
  9885. }
  9886. // inject css
  9887. util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
  9888. })()
  9889. },{"moment":2}],2:[function(require,module,exports){
  9890. (function(){// moment.js
  9891. // version : 2.0.0
  9892. // author : Tim Wood
  9893. // license : MIT
  9894. // momentjs.com
  9895. (function (undefined) {
  9896. /************************************
  9897. Constants
  9898. ************************************/
  9899. var moment,
  9900. VERSION = "2.0.0",
  9901. round = Math.round, i,
  9902. // internal storage for language config files
  9903. languages = {},
  9904. // check for nodeJS
  9905. hasModule = (typeof module !== 'undefined' && module.exports),
  9906. // ASP.NET json date format regex
  9907. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  9908. // format tokens
  9909. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
  9910. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  9911. // parsing tokens
  9912. parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  9913. // parsing token regexes
  9914. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  9915. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  9916. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  9917. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  9918. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  9919. parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic.
  9920. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  9921. parseTokenT = /T/i, // T (ISO seperator)
  9922. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  9923. // preliminary iso regex
  9924. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  9925. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  9926. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  9927. // iso time formats and regexes
  9928. isoTimes = [
  9929. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  9930. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  9931. ['HH:mm', /(T| )\d\d:\d\d/],
  9932. ['HH', /(T| )\d\d/]
  9933. ],
  9934. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  9935. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  9936. // getter and setter names
  9937. proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  9938. unitMillisecondFactors = {
  9939. 'Milliseconds' : 1,
  9940. 'Seconds' : 1e3,
  9941. 'Minutes' : 6e4,
  9942. 'Hours' : 36e5,
  9943. 'Days' : 864e5,
  9944. 'Months' : 2592e6,
  9945. 'Years' : 31536e6
  9946. },
  9947. // format function strings
  9948. formatFunctions = {},
  9949. // tokens to ordinalize and pad
  9950. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  9951. paddedTokens = 'M D H h m s w W'.split(' '),
  9952. formatTokenFunctions = {
  9953. M : function () {
  9954. return this.month() + 1;
  9955. },
  9956. MMM : function (format) {
  9957. return this.lang().monthsShort(this, format);
  9958. },
  9959. MMMM : function (format) {
  9960. return this.lang().months(this, format);
  9961. },
  9962. D : function () {
  9963. return this.date();
  9964. },
  9965. DDD : function () {
  9966. return this.dayOfYear();
  9967. },
  9968. d : function () {
  9969. return this.day();
  9970. },
  9971. dd : function (format) {
  9972. return this.lang().weekdaysMin(this, format);
  9973. },
  9974. ddd : function (format) {
  9975. return this.lang().weekdaysShort(this, format);
  9976. },
  9977. dddd : function (format) {
  9978. return this.lang().weekdays(this, format);
  9979. },
  9980. w : function () {
  9981. return this.week();
  9982. },
  9983. W : function () {
  9984. return this.isoWeek();
  9985. },
  9986. YY : function () {
  9987. return leftZeroFill(this.year() % 100, 2);
  9988. },
  9989. YYYY : function () {
  9990. return leftZeroFill(this.year(), 4);
  9991. },
  9992. YYYYY : function () {
  9993. return leftZeroFill(this.year(), 5);
  9994. },
  9995. a : function () {
  9996. return this.lang().meridiem(this.hours(), this.minutes(), true);
  9997. },
  9998. A : function () {
  9999. return this.lang().meridiem(this.hours(), this.minutes(), false);
  10000. },
  10001. H : function () {
  10002. return this.hours();
  10003. },
  10004. h : function () {
  10005. return this.hours() % 12 || 12;
  10006. },
  10007. m : function () {
  10008. return this.minutes();
  10009. },
  10010. s : function () {
  10011. return this.seconds();
  10012. },
  10013. S : function () {
  10014. return ~~(this.milliseconds() / 100);
  10015. },
  10016. SS : function () {
  10017. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  10018. },
  10019. SSS : function () {
  10020. return leftZeroFill(this.milliseconds(), 3);
  10021. },
  10022. Z : function () {
  10023. var a = -this.zone(),
  10024. b = "+";
  10025. if (a < 0) {
  10026. a = -a;
  10027. b = "-";
  10028. }
  10029. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  10030. },
  10031. ZZ : function () {
  10032. var a = -this.zone(),
  10033. b = "+";
  10034. if (a < 0) {
  10035. a = -a;
  10036. b = "-";
  10037. }
  10038. return b + leftZeroFill(~~(10 * a / 6), 4);
  10039. },
  10040. X : function () {
  10041. return this.unix();
  10042. }
  10043. };
  10044. function padToken(func, count) {
  10045. return function (a) {
  10046. return leftZeroFill(func.call(this, a), count);
  10047. };
  10048. }
  10049. function ordinalizeToken(func) {
  10050. return function (a) {
  10051. return this.lang().ordinal(func.call(this, a));
  10052. };
  10053. }
  10054. while (ordinalizeTokens.length) {
  10055. i = ordinalizeTokens.pop();
  10056. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  10057. }
  10058. while (paddedTokens.length) {
  10059. i = paddedTokens.pop();
  10060. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  10061. }
  10062. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  10063. /************************************
  10064. Constructors
  10065. ************************************/
  10066. function Language() {
  10067. }
  10068. // Moment prototype object
  10069. function Moment(config) {
  10070. extend(this, config);
  10071. }
  10072. // Duration Constructor
  10073. function Duration(duration) {
  10074. var data = this._data = {},
  10075. years = duration.years || duration.year || duration.y || 0,
  10076. months = duration.months || duration.month || duration.M || 0,
  10077. weeks = duration.weeks || duration.week || duration.w || 0,
  10078. days = duration.days || duration.day || duration.d || 0,
  10079. hours = duration.hours || duration.hour || duration.h || 0,
  10080. minutes = duration.minutes || duration.minute || duration.m || 0,
  10081. seconds = duration.seconds || duration.second || duration.s || 0,
  10082. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  10083. // representation for dateAddRemove
  10084. this._milliseconds = milliseconds +
  10085. seconds * 1e3 + // 1000
  10086. minutes * 6e4 + // 1000 * 60
  10087. hours * 36e5; // 1000 * 60 * 60
  10088. // Because of dateAddRemove treats 24 hours as different from a
  10089. // day when working around DST, we need to store them separately
  10090. this._days = days +
  10091. weeks * 7;
  10092. // It is impossible translate months into days without knowing
  10093. // which months you are are talking about, so we have to store
  10094. // it separately.
  10095. this._months = months +
  10096. years * 12;
  10097. // The following code bubbles up values, see the tests for
  10098. // examples of what that means.
  10099. data.milliseconds = milliseconds % 1000;
  10100. seconds += absRound(milliseconds / 1000);
  10101. data.seconds = seconds % 60;
  10102. minutes += absRound(seconds / 60);
  10103. data.minutes = minutes % 60;
  10104. hours += absRound(minutes / 60);
  10105. data.hours = hours % 24;
  10106. days += absRound(hours / 24);
  10107. days += weeks * 7;
  10108. data.days = days % 30;
  10109. months += absRound(days / 30);
  10110. data.months = months % 12;
  10111. years += absRound(months / 12);
  10112. data.years = years;
  10113. }
  10114. /************************************
  10115. Helpers
  10116. ************************************/
  10117. function extend(a, b) {
  10118. for (var i in b) {
  10119. if (b.hasOwnProperty(i)) {
  10120. a[i] = b[i];
  10121. }
  10122. }
  10123. return a;
  10124. }
  10125. function absRound(number) {
  10126. if (number < 0) {
  10127. return Math.ceil(number);
  10128. } else {
  10129. return Math.floor(number);
  10130. }
  10131. }
  10132. // left zero fill a number
  10133. // see http://jsperf.com/left-zero-filling for performance comparison
  10134. function leftZeroFill(number, targetLength) {
  10135. var output = number + '';
  10136. while (output.length < targetLength) {
  10137. output = '0' + output;
  10138. }
  10139. return output;
  10140. }
  10141. // helper function for _.addTime and _.subtractTime
  10142. function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  10143. var ms = duration._milliseconds,
  10144. d = duration._days,
  10145. M = duration._months,
  10146. currentDate;
  10147. if (ms) {
  10148. mom._d.setTime(+mom + ms * isAdding);
  10149. }
  10150. if (d) {
  10151. mom.date(mom.date() + d * isAdding);
  10152. }
  10153. if (M) {
  10154. currentDate = mom.date();
  10155. mom.date(1)
  10156. .month(mom.month() + M * isAdding)
  10157. .date(Math.min(currentDate, mom.daysInMonth()));
  10158. }
  10159. }
  10160. // check if is an array
  10161. function isArray(input) {
  10162. return Object.prototype.toString.call(input) === '[object Array]';
  10163. }
  10164. // compare two arrays, return the number of differences
  10165. function compareArrays(array1, array2) {
  10166. var len = Math.min(array1.length, array2.length),
  10167. lengthDiff = Math.abs(array1.length - array2.length),
  10168. diffs = 0,
  10169. i;
  10170. for (i = 0; i < len; i++) {
  10171. if (~~array1[i] !== ~~array2[i]) {
  10172. diffs++;
  10173. }
  10174. }
  10175. return diffs + lengthDiff;
  10176. }
  10177. /************************************
  10178. Languages
  10179. ************************************/
  10180. Language.prototype = {
  10181. set : function (config) {
  10182. var prop, i;
  10183. for (i in config) {
  10184. prop = config[i];
  10185. if (typeof prop === 'function') {
  10186. this[i] = prop;
  10187. } else {
  10188. this['_' + i] = prop;
  10189. }
  10190. }
  10191. },
  10192. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  10193. months : function (m) {
  10194. return this._months[m.month()];
  10195. },
  10196. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  10197. monthsShort : function (m) {
  10198. return this._monthsShort[m.month()];
  10199. },
  10200. monthsParse : function (monthName) {
  10201. var i, mom, regex, output;
  10202. if (!this._monthsParse) {
  10203. this._monthsParse = [];
  10204. }
  10205. for (i = 0; i < 12; i++) {
  10206. // make the regex if we don't have it already
  10207. if (!this._monthsParse[i]) {
  10208. mom = moment([2000, i]);
  10209. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  10210. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  10211. }
  10212. // test the regex
  10213. if (this._monthsParse[i].test(monthName)) {
  10214. return i;
  10215. }
  10216. }
  10217. },
  10218. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  10219. weekdays : function (m) {
  10220. return this._weekdays[m.day()];
  10221. },
  10222. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  10223. weekdaysShort : function (m) {
  10224. return this._weekdaysShort[m.day()];
  10225. },
  10226. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  10227. weekdaysMin : function (m) {
  10228. return this._weekdaysMin[m.day()];
  10229. },
  10230. _longDateFormat : {
  10231. LT : "h:mm A",
  10232. L : "MM/DD/YYYY",
  10233. LL : "MMMM D YYYY",
  10234. LLL : "MMMM D YYYY LT",
  10235. LLLL : "dddd, MMMM D YYYY LT"
  10236. },
  10237. longDateFormat : function (key) {
  10238. var output = this._longDateFormat[key];
  10239. if (!output && this._longDateFormat[key.toUpperCase()]) {
  10240. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  10241. return val.slice(1);
  10242. });
  10243. this._longDateFormat[key] = output;
  10244. }
  10245. return output;
  10246. },
  10247. meridiem : function (hours, minutes, isLower) {
  10248. if (hours > 11) {
  10249. return isLower ? 'pm' : 'PM';
  10250. } else {
  10251. return isLower ? 'am' : 'AM';
  10252. }
  10253. },
  10254. _calendar : {
  10255. sameDay : '[Today at] LT',
  10256. nextDay : '[Tomorrow at] LT',
  10257. nextWeek : 'dddd [at] LT',
  10258. lastDay : '[Yesterday at] LT',
  10259. lastWeek : '[last] dddd [at] LT',
  10260. sameElse : 'L'
  10261. },
  10262. calendar : function (key, mom) {
  10263. var output = this._calendar[key];
  10264. return typeof output === 'function' ? output.apply(mom) : output;
  10265. },
  10266. _relativeTime : {
  10267. future : "in %s",
  10268. past : "%s ago",
  10269. s : "a few seconds",
  10270. m : "a minute",
  10271. mm : "%d minutes",
  10272. h : "an hour",
  10273. hh : "%d hours",
  10274. d : "a day",
  10275. dd : "%d days",
  10276. M : "a month",
  10277. MM : "%d months",
  10278. y : "a year",
  10279. yy : "%d years"
  10280. },
  10281. relativeTime : function (number, withoutSuffix, string, isFuture) {
  10282. var output = this._relativeTime[string];
  10283. return (typeof output === 'function') ?
  10284. output(number, withoutSuffix, string, isFuture) :
  10285. output.replace(/%d/i, number);
  10286. },
  10287. pastFuture : function (diff, output) {
  10288. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  10289. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  10290. },
  10291. ordinal : function (number) {
  10292. return this._ordinal.replace("%d", number);
  10293. },
  10294. _ordinal : "%d",
  10295. preparse : function (string) {
  10296. return string;
  10297. },
  10298. postformat : function (string) {
  10299. return string;
  10300. },
  10301. week : function (mom) {
  10302. return weekOfYear(mom, this._week.dow, this._week.doy);
  10303. },
  10304. _week : {
  10305. dow : 0, // Sunday is the first day of the week.
  10306. doy : 6 // The week that contains Jan 1st is the first week of the year.
  10307. }
  10308. };
  10309. // Loads a language definition into the `languages` cache. The function
  10310. // takes a key and optionally values. If not in the browser and no values
  10311. // are provided, it will load the language file module. As a convenience,
  10312. // this function also returns the language values.
  10313. function loadLang(key, values) {
  10314. values.abbr = key;
  10315. if (!languages[key]) {
  10316. languages[key] = new Language();
  10317. }
  10318. languages[key].set(values);
  10319. return languages[key];
  10320. }
  10321. // Determines which language definition to use and returns it.
  10322. //
  10323. // With no parameters, it will return the global language. If you
  10324. // pass in a language key, such as 'en', it will return the
  10325. // definition for 'en', so long as 'en' has already been loaded using
  10326. // moment.lang.
  10327. function getLangDefinition(key) {
  10328. if (!key) {
  10329. return moment.fn._lang;
  10330. }
  10331. if (!languages[key] && hasModule) {
  10332. require('./lang/' + key);
  10333. }
  10334. return languages[key];
  10335. }
  10336. /************************************
  10337. Formatting
  10338. ************************************/
  10339. function removeFormattingTokens(input) {
  10340. if (input.match(/\[.*\]/)) {
  10341. return input.replace(/^\[|\]$/g, "");
  10342. }
  10343. return input.replace(/\\/g, "");
  10344. }
  10345. function makeFormatFunction(format) {
  10346. var array = format.match(formattingTokens), i, length;
  10347. for (i = 0, length = array.length; i < length; i++) {
  10348. if (formatTokenFunctions[array[i]]) {
  10349. array[i] = formatTokenFunctions[array[i]];
  10350. } else {
  10351. array[i] = removeFormattingTokens(array[i]);
  10352. }
  10353. }
  10354. return function (mom) {
  10355. var output = "";
  10356. for (i = 0; i < length; i++) {
  10357. output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  10358. }
  10359. return output;
  10360. };
  10361. }
  10362. // format date using native date object
  10363. function formatMoment(m, format) {
  10364. var i = 5;
  10365. function replaceLongDateFormatTokens(input) {
  10366. return m.lang().longDateFormat(input) || input;
  10367. }
  10368. while (i-- && localFormattingTokens.test(format)) {
  10369. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  10370. }
  10371. if (!formatFunctions[format]) {
  10372. formatFunctions[format] = makeFormatFunction(format);
  10373. }
  10374. return formatFunctions[format](m);
  10375. }
  10376. /************************************
  10377. Parsing
  10378. ************************************/
  10379. // get the regex to find the next token
  10380. function getParseRegexForToken(token) {
  10381. switch (token) {
  10382. case 'DDDD':
  10383. return parseTokenThreeDigits;
  10384. case 'YYYY':
  10385. return parseTokenFourDigits;
  10386. case 'YYYYY':
  10387. return parseTokenSixDigits;
  10388. case 'S':
  10389. case 'SS':
  10390. case 'SSS':
  10391. case 'DDD':
  10392. return parseTokenOneToThreeDigits;
  10393. case 'MMM':
  10394. case 'MMMM':
  10395. case 'dd':
  10396. case 'ddd':
  10397. case 'dddd':
  10398. case 'a':
  10399. case 'A':
  10400. return parseTokenWord;
  10401. case 'X':
  10402. return parseTokenTimestampMs;
  10403. case 'Z':
  10404. case 'ZZ':
  10405. return parseTokenTimezone;
  10406. case 'T':
  10407. return parseTokenT;
  10408. case 'MM':
  10409. case 'DD':
  10410. case 'YY':
  10411. case 'HH':
  10412. case 'hh':
  10413. case 'mm':
  10414. case 'ss':
  10415. case 'M':
  10416. case 'D':
  10417. case 'd':
  10418. case 'H':
  10419. case 'h':
  10420. case 'm':
  10421. case 's':
  10422. return parseTokenOneOrTwoDigits;
  10423. default :
  10424. return new RegExp(token.replace('\\', ''));
  10425. }
  10426. }
  10427. // function to convert string input to date
  10428. function addTimeToArrayFromToken(token, input, config) {
  10429. var a, b,
  10430. datePartArray = config._a;
  10431. switch (token) {
  10432. // MONTH
  10433. case 'M' : // fall through to MM
  10434. case 'MM' :
  10435. datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  10436. break;
  10437. case 'MMM' : // fall through to MMMM
  10438. case 'MMMM' :
  10439. a = getLangDefinition(config._l).monthsParse(input);
  10440. // if we didn't find a month name, mark the date as invalid.
  10441. if (a != null) {
  10442. datePartArray[1] = a;
  10443. } else {
  10444. config._isValid = false;
  10445. }
  10446. break;
  10447. // DAY OF MONTH
  10448. case 'D' : // fall through to DDDD
  10449. case 'DD' : // fall through to DDDD
  10450. case 'DDD' : // fall through to DDDD
  10451. case 'DDDD' :
  10452. if (input != null) {
  10453. datePartArray[2] = ~~input;
  10454. }
  10455. break;
  10456. // YEAR
  10457. case 'YY' :
  10458. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  10459. break;
  10460. case 'YYYY' :
  10461. case 'YYYYY' :
  10462. datePartArray[0] = ~~input;
  10463. break;
  10464. // AM / PM
  10465. case 'a' : // fall through to A
  10466. case 'A' :
  10467. config._isPm = ((input + '').toLowerCase() === 'pm');
  10468. break;
  10469. // 24 HOUR
  10470. case 'H' : // fall through to hh
  10471. case 'HH' : // fall through to hh
  10472. case 'h' : // fall through to hh
  10473. case 'hh' :
  10474. datePartArray[3] = ~~input;
  10475. break;
  10476. // MINUTE
  10477. case 'm' : // fall through to mm
  10478. case 'mm' :
  10479. datePartArray[4] = ~~input;
  10480. break;
  10481. // SECOND
  10482. case 's' : // fall through to ss
  10483. case 'ss' :
  10484. datePartArray[5] = ~~input;
  10485. break;
  10486. // MILLISECOND
  10487. case 'S' :
  10488. case 'SS' :
  10489. case 'SSS' :
  10490. datePartArray[6] = ~~ (('0.' + input) * 1000);
  10491. break;
  10492. // UNIX TIMESTAMP WITH MS
  10493. case 'X':
  10494. config._d = new Date(parseFloat(input) * 1000);
  10495. break;
  10496. // TIMEZONE
  10497. case 'Z' : // fall through to ZZ
  10498. case 'ZZ' :
  10499. config._useUTC = true;
  10500. a = (input + '').match(parseTimezoneChunker);
  10501. if (a && a[1]) {
  10502. config._tzh = ~~a[1];
  10503. }
  10504. if (a && a[2]) {
  10505. config._tzm = ~~a[2];
  10506. }
  10507. // reverse offsets
  10508. if (a && a[0] === '+') {
  10509. config._tzh = -config._tzh;
  10510. config._tzm = -config._tzm;
  10511. }
  10512. break;
  10513. }
  10514. // if the input is null, the date is not valid
  10515. if (input == null) {
  10516. config._isValid = false;
  10517. }
  10518. }
  10519. // convert an array to a date.
  10520. // the array should mirror the parameters below
  10521. // note: all values past the year are optional and will default to the lowest possible value.
  10522. // [year, month, day , hour, minute, second, millisecond]
  10523. function dateFromArray(config) {
  10524. var i, date, input = [];
  10525. if (config._d) {
  10526. return;
  10527. }
  10528. for (i = 0; i < 7; i++) {
  10529. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  10530. }
  10531. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  10532. input[3] += config._tzh || 0;
  10533. input[4] += config._tzm || 0;
  10534. date = new Date(0);
  10535. if (config._useUTC) {
  10536. date.setUTCFullYear(input[0], input[1], input[2]);
  10537. date.setUTCHours(input[3], input[4], input[5], input[6]);
  10538. } else {
  10539. date.setFullYear(input[0], input[1], input[2]);
  10540. date.setHours(input[3], input[4], input[5], input[6]);
  10541. }
  10542. config._d = date;
  10543. }
  10544. // date from string and format string
  10545. function makeDateFromStringAndFormat(config) {
  10546. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  10547. var tokens = config._f.match(formattingTokens),
  10548. string = config._i,
  10549. i, parsedInput;
  10550. config._a = [];
  10551. for (i = 0; i < tokens.length; i++) {
  10552. parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  10553. if (parsedInput) {
  10554. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  10555. }
  10556. // don't parse if its not a known token
  10557. if (formatTokenFunctions[tokens[i]]) {
  10558. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  10559. }
  10560. }
  10561. // handle am pm
  10562. if (config._isPm && config._a[3] < 12) {
  10563. config._a[3] += 12;
  10564. }
  10565. // if is 12 am, change hours to 0
  10566. if (config._isPm === false && config._a[3] === 12) {
  10567. config._a[3] = 0;
  10568. }
  10569. // return
  10570. dateFromArray(config);
  10571. }
  10572. // date from string and array of format strings
  10573. function makeDateFromStringAndArray(config) {
  10574. var tempConfig,
  10575. tempMoment,
  10576. bestMoment,
  10577. scoreToBeat = 99,
  10578. i,
  10579. currentDate,
  10580. currentScore;
  10581. while (config._f.length) {
  10582. tempConfig = extend({}, config);
  10583. tempConfig._f = config._f.pop();
  10584. makeDateFromStringAndFormat(tempConfig);
  10585. tempMoment = new Moment(tempConfig);
  10586. if (tempMoment.isValid()) {
  10587. bestMoment = tempMoment;
  10588. break;
  10589. }
  10590. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  10591. if (currentScore < scoreToBeat) {
  10592. scoreToBeat = currentScore;
  10593. bestMoment = tempMoment;
  10594. }
  10595. }
  10596. extend(config, bestMoment);
  10597. }
  10598. // date from iso format
  10599. function makeDateFromString(config) {
  10600. var i,
  10601. string = config._i;
  10602. if (isoRegex.exec(string)) {
  10603. config._f = 'YYYY-MM-DDT';
  10604. for (i = 0; i < 4; i++) {
  10605. if (isoTimes[i][1].exec(string)) {
  10606. config._f += isoTimes[i][0];
  10607. break;
  10608. }
  10609. }
  10610. if (parseTokenTimezone.exec(string)) {
  10611. config._f += " Z";
  10612. }
  10613. makeDateFromStringAndFormat(config);
  10614. } else {
  10615. config._d = new Date(string);
  10616. }
  10617. }
  10618. function makeDateFromInput(config) {
  10619. var input = config._i,
  10620. matched = aspNetJsonRegex.exec(input);
  10621. if (input === undefined) {
  10622. config._d = new Date();
  10623. } else if (matched) {
  10624. config._d = new Date(+matched[1]);
  10625. } else if (typeof input === 'string') {
  10626. makeDateFromString(config);
  10627. } else if (isArray(input)) {
  10628. config._a = input.slice(0);
  10629. dateFromArray(config);
  10630. } else {
  10631. config._d = input instanceof Date ? new Date(+input) : new Date(input);
  10632. }
  10633. }
  10634. /************************************
  10635. Relative Time
  10636. ************************************/
  10637. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  10638. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  10639. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  10640. }
  10641. function relativeTime(milliseconds, withoutSuffix, lang) {
  10642. var seconds = round(Math.abs(milliseconds) / 1000),
  10643. minutes = round(seconds / 60),
  10644. hours = round(minutes / 60),
  10645. days = round(hours / 24),
  10646. years = round(days / 365),
  10647. args = seconds < 45 && ['s', seconds] ||
  10648. minutes === 1 && ['m'] ||
  10649. minutes < 45 && ['mm', minutes] ||
  10650. hours === 1 && ['h'] ||
  10651. hours < 22 && ['hh', hours] ||
  10652. days === 1 && ['d'] ||
  10653. days <= 25 && ['dd', days] ||
  10654. days <= 45 && ['M'] ||
  10655. days < 345 && ['MM', round(days / 30)] ||
  10656. years === 1 && ['y'] || ['yy', years];
  10657. args[2] = withoutSuffix;
  10658. args[3] = milliseconds > 0;
  10659. args[4] = lang;
  10660. return substituteTimeAgo.apply({}, args);
  10661. }
  10662. /************************************
  10663. Week of Year
  10664. ************************************/
  10665. // firstDayOfWeek 0 = sun, 6 = sat
  10666. // the day of the week that starts the week
  10667. // (usually sunday or monday)
  10668. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  10669. // the first week is the week that contains the first
  10670. // of this day of the week
  10671. // (eg. ISO weeks use thursday (4))
  10672. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  10673. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  10674. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  10675. if (daysToDayOfWeek > end) {
  10676. daysToDayOfWeek -= 7;
  10677. }
  10678. if (daysToDayOfWeek < end - 7) {
  10679. daysToDayOfWeek += 7;
  10680. }
  10681. return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  10682. }
  10683. /************************************
  10684. Top Level Functions
  10685. ************************************/
  10686. function makeMoment(config) {
  10687. var input = config._i,
  10688. format = config._f;
  10689. if (input === null || input === '') {
  10690. return null;
  10691. }
  10692. if (typeof input === 'string') {
  10693. config._i = input = getLangDefinition().preparse(input);
  10694. }
  10695. if (moment.isMoment(input)) {
  10696. config = extend({}, input);
  10697. config._d = new Date(+input._d);
  10698. } else if (format) {
  10699. if (isArray(format)) {
  10700. makeDateFromStringAndArray(config);
  10701. } else {
  10702. makeDateFromStringAndFormat(config);
  10703. }
  10704. } else {
  10705. makeDateFromInput(config);
  10706. }
  10707. return new Moment(config);
  10708. }
  10709. moment = function (input, format, lang) {
  10710. return makeMoment({
  10711. _i : input,
  10712. _f : format,
  10713. _l : lang,
  10714. _isUTC : false
  10715. });
  10716. };
  10717. // creating with utc
  10718. moment.utc = function (input, format, lang) {
  10719. return makeMoment({
  10720. _useUTC : true,
  10721. _isUTC : true,
  10722. _l : lang,
  10723. _i : input,
  10724. _f : format
  10725. });
  10726. };
  10727. // creating with unix timestamp (in seconds)
  10728. moment.unix = function (input) {
  10729. return moment(input * 1000);
  10730. };
  10731. // duration
  10732. moment.duration = function (input, key) {
  10733. var isDuration = moment.isDuration(input),
  10734. isNumber = (typeof input === 'number'),
  10735. duration = (isDuration ? input._data : (isNumber ? {} : input)),
  10736. ret;
  10737. if (isNumber) {
  10738. if (key) {
  10739. duration[key] = input;
  10740. } else {
  10741. duration.milliseconds = input;
  10742. }
  10743. }
  10744. ret = new Duration(duration);
  10745. if (isDuration && input.hasOwnProperty('_lang')) {
  10746. ret._lang = input._lang;
  10747. }
  10748. return ret;
  10749. };
  10750. // version number
  10751. moment.version = VERSION;
  10752. // default format
  10753. moment.defaultFormat = isoFormat;
  10754. // This function will load languages and then set the global language. If
  10755. // no arguments are passed in, it will simply return the current global
  10756. // language key.
  10757. moment.lang = function (key, values) {
  10758. var i;
  10759. if (!key) {
  10760. return moment.fn._lang._abbr;
  10761. }
  10762. if (values) {
  10763. loadLang(key, values);
  10764. } else if (!languages[key]) {
  10765. getLangDefinition(key);
  10766. }
  10767. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  10768. };
  10769. // returns language data
  10770. moment.langData = function (key) {
  10771. if (key && key._lang && key._lang._abbr) {
  10772. key = key._lang._abbr;
  10773. }
  10774. return getLangDefinition(key);
  10775. };
  10776. // compare moment object
  10777. moment.isMoment = function (obj) {
  10778. return obj instanceof Moment;
  10779. };
  10780. // for typechecking Duration objects
  10781. moment.isDuration = function (obj) {
  10782. return obj instanceof Duration;
  10783. };
  10784. /************************************
  10785. Moment Prototype
  10786. ************************************/
  10787. moment.fn = Moment.prototype = {
  10788. clone : function () {
  10789. return moment(this);
  10790. },
  10791. valueOf : function () {
  10792. return +this._d;
  10793. },
  10794. unix : function () {
  10795. return Math.floor(+this._d / 1000);
  10796. },
  10797. toString : function () {
  10798. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  10799. },
  10800. toDate : function () {
  10801. return this._d;
  10802. },
  10803. toJSON : function () {
  10804. return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  10805. },
  10806. toArray : function () {
  10807. var m = this;
  10808. return [
  10809. m.year(),
  10810. m.month(),
  10811. m.date(),
  10812. m.hours(),
  10813. m.minutes(),
  10814. m.seconds(),
  10815. m.milliseconds()
  10816. ];
  10817. },
  10818. isValid : function () {
  10819. if (this._isValid == null) {
  10820. if (this._a) {
  10821. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  10822. } else {
  10823. this._isValid = !isNaN(this._d.getTime());
  10824. }
  10825. }
  10826. return !!this._isValid;
  10827. },
  10828. utc : function () {
  10829. this._isUTC = true;
  10830. return this;
  10831. },
  10832. local : function () {
  10833. this._isUTC = false;
  10834. return this;
  10835. },
  10836. format : function (inputString) {
  10837. var output = formatMoment(this, inputString || moment.defaultFormat);
  10838. return this.lang().postformat(output);
  10839. },
  10840. add : function (input, val) {
  10841. var dur;
  10842. // switch args to support add('s', 1) and add(1, 's')
  10843. if (typeof input === 'string') {
  10844. dur = moment.duration(+val, input);
  10845. } else {
  10846. dur = moment.duration(input, val);
  10847. }
  10848. addOrSubtractDurationFromMoment(this, dur, 1);
  10849. return this;
  10850. },
  10851. subtract : function (input, val) {
  10852. var dur;
  10853. // switch args to support subtract('s', 1) and subtract(1, 's')
  10854. if (typeof input === 'string') {
  10855. dur = moment.duration(+val, input);
  10856. } else {
  10857. dur = moment.duration(input, val);
  10858. }
  10859. addOrSubtractDurationFromMoment(this, dur, -1);
  10860. return this;
  10861. },
  10862. diff : function (input, units, asFloat) {
  10863. var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  10864. zoneDiff = (this.zone() - that.zone()) * 6e4,
  10865. diff, output;
  10866. if (units) {
  10867. // standardize on singular form
  10868. units = units.replace(/s$/, '');
  10869. }
  10870. if (units === 'year' || units === 'month') {
  10871. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  10872. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  10873. output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
  10874. if (units === 'year') {
  10875. output = output / 12;
  10876. }
  10877. } else {
  10878. diff = (this - that) - zoneDiff;
  10879. output = units === 'second' ? diff / 1e3 : // 1000
  10880. units === 'minute' ? diff / 6e4 : // 1000 * 60
  10881. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  10882. units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
  10883. units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
  10884. diff;
  10885. }
  10886. return asFloat ? output : absRound(output);
  10887. },
  10888. from : function (time, withoutSuffix) {
  10889. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  10890. },
  10891. fromNow : function (withoutSuffix) {
  10892. return this.from(moment(), withoutSuffix);
  10893. },
  10894. calendar : function () {
  10895. var diff = this.diff(moment().startOf('day'), 'days', true),
  10896. format = diff < -6 ? 'sameElse' :
  10897. diff < -1 ? 'lastWeek' :
  10898. diff < 0 ? 'lastDay' :
  10899. diff < 1 ? 'sameDay' :
  10900. diff < 2 ? 'nextDay' :
  10901. diff < 7 ? 'nextWeek' : 'sameElse';
  10902. return this.format(this.lang().calendar(format, this));
  10903. },
  10904. isLeapYear : function () {
  10905. var year = this.year();
  10906. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  10907. },
  10908. isDST : function () {
  10909. return (this.zone() < moment([this.year()]).zone() ||
  10910. this.zone() < moment([this.year(), 5]).zone());
  10911. },
  10912. day : function (input) {
  10913. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  10914. return input == null ? day :
  10915. this.add({ d : input - day });
  10916. },
  10917. startOf: function (units) {
  10918. units = units.replace(/s$/, '');
  10919. // the following switch intentionally omits break keywords
  10920. // to utilize falling through the cases.
  10921. switch (units) {
  10922. case 'year':
  10923. this.month(0);
  10924. /* falls through */
  10925. case 'month':
  10926. this.date(1);
  10927. /* falls through */
  10928. case 'week':
  10929. case 'day':
  10930. this.hours(0);
  10931. /* falls through */
  10932. case 'hour':
  10933. this.minutes(0);
  10934. /* falls through */
  10935. case 'minute':
  10936. this.seconds(0);
  10937. /* falls through */
  10938. case 'second':
  10939. this.milliseconds(0);
  10940. /* falls through */
  10941. }
  10942. // weeks are a special case
  10943. if (units === 'week') {
  10944. this.day(0);
  10945. }
  10946. return this;
  10947. },
  10948. endOf: function (units) {
  10949. return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
  10950. },
  10951. isAfter: function (input, units) {
  10952. units = typeof units !== 'undefined' ? units : 'millisecond';
  10953. return +this.clone().startOf(units) > +moment(input).startOf(units);
  10954. },
  10955. isBefore: function (input, units) {
  10956. units = typeof units !== 'undefined' ? units : 'millisecond';
  10957. return +this.clone().startOf(units) < +moment(input).startOf(units);
  10958. },
  10959. isSame: function (input, units) {
  10960. units = typeof units !== 'undefined' ? units : 'millisecond';
  10961. return +this.clone().startOf(units) === +moment(input).startOf(units);
  10962. },
  10963. zone : function () {
  10964. return this._isUTC ? 0 : this._d.getTimezoneOffset();
  10965. },
  10966. daysInMonth : function () {
  10967. return moment.utc([this.year(), this.month() + 1, 0]).date();
  10968. },
  10969. dayOfYear : function (input) {
  10970. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  10971. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  10972. },
  10973. isoWeek : function (input) {
  10974. var week = weekOfYear(this, 1, 4);
  10975. return input == null ? week : this.add("d", (input - week) * 7);
  10976. },
  10977. week : function (input) {
  10978. var week = this.lang().week(this);
  10979. return input == null ? week : this.add("d", (input - week) * 7);
  10980. },
  10981. // If passed a language key, it will set the language for this
  10982. // instance. Otherwise, it will return the language configuration
  10983. // variables for this instance.
  10984. lang : function (key) {
  10985. if (key === undefined) {
  10986. return this._lang;
  10987. } else {
  10988. this._lang = getLangDefinition(key);
  10989. return this;
  10990. }
  10991. }
  10992. };
  10993. // helper for adding shortcuts
  10994. function makeGetterAndSetter(name, key) {
  10995. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  10996. var utc = this._isUTC ? 'UTC' : '';
  10997. if (input != null) {
  10998. this._d['set' + utc + key](input);
  10999. return this;
  11000. } else {
  11001. return this._d['get' + utc + key]();
  11002. }
  11003. };
  11004. }
  11005. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  11006. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  11007. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  11008. }
  11009. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  11010. makeGetterAndSetter('year', 'FullYear');
  11011. // add plural methods
  11012. moment.fn.days = moment.fn.day;
  11013. moment.fn.weeks = moment.fn.week;
  11014. moment.fn.isoWeeks = moment.fn.isoWeek;
  11015. /************************************
  11016. Duration Prototype
  11017. ************************************/
  11018. moment.duration.fn = Duration.prototype = {
  11019. weeks : function () {
  11020. return absRound(this.days() / 7);
  11021. },
  11022. valueOf : function () {
  11023. return this._milliseconds +
  11024. this._days * 864e5 +
  11025. this._months * 2592e6;
  11026. },
  11027. humanize : function (withSuffix) {
  11028. var difference = +this,
  11029. output = relativeTime(difference, !withSuffix, this.lang());
  11030. if (withSuffix) {
  11031. output = this.lang().pastFuture(difference, output);
  11032. }
  11033. return this.lang().postformat(output);
  11034. },
  11035. lang : moment.fn.lang
  11036. };
  11037. function makeDurationGetter(name) {
  11038. moment.duration.fn[name] = function () {
  11039. return this._data[name];
  11040. };
  11041. }
  11042. function makeDurationAsGetter(name, factor) {
  11043. moment.duration.fn['as' + name] = function () {
  11044. return +this / factor;
  11045. };
  11046. }
  11047. for (i in unitMillisecondFactors) {
  11048. if (unitMillisecondFactors.hasOwnProperty(i)) {
  11049. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  11050. makeDurationGetter(i.toLowerCase());
  11051. }
  11052. }
  11053. makeDurationAsGetter('Weeks', 6048e5);
  11054. /************************************
  11055. Default Lang
  11056. ************************************/
  11057. // Set default language, other languages will inherit from English.
  11058. moment.lang('en', {
  11059. ordinal : function (number) {
  11060. var b = number % 10,
  11061. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  11062. (b === 1) ? 'st' :
  11063. (b === 2) ? 'nd' :
  11064. (b === 3) ? 'rd' : 'th';
  11065. return number + output;
  11066. }
  11067. });
  11068. /************************************
  11069. Exposing Moment
  11070. ************************************/
  11071. // CommonJS module is defined
  11072. if (hasModule) {
  11073. module.exports = moment;
  11074. }
  11075. /*global ender:false */
  11076. if (typeof ender === 'undefined') {
  11077. // here, `this` means `window` in the browser, or `global` on the server
  11078. // add `moment` as a global object via a string identifier,
  11079. // for Closure Compiler "advanced" mode
  11080. this['moment'] = moment;
  11081. }
  11082. /*global define:false */
  11083. if (typeof define === "function" && define.amd) {
  11084. define("moment", [], function () {
  11085. return moment;
  11086. });
  11087. }
  11088. }).call(this);
  11089. })()
  11090. },{}]},{},[1])(1)
  11091. });
  11092. ;