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.

6495 lines
198 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
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.0.5
  8. * @date 2013-04-23
  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 () {
  26. // Define namespace vis
  27. var vis = {
  28. component: {
  29. item: {}
  30. }
  31. };
  32. /**
  33. * load css from text
  34. * @param {String} css Text containing css
  35. */
  36. var loadCss = function (css) {
  37. // get the script location, and built the css file name from the js file name
  38. // http://stackoverflow.com/a/2161748/1262753
  39. var scripts = document.getElementsByTagName('script');
  40. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  41. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  42. // inject css
  43. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  44. var style = document.createElement('style');
  45. style.type = 'text/css';
  46. if (style.styleSheet){
  47. style.styleSheet.cssText = css;
  48. } else {
  49. style.appendChild(document.createTextNode(css));
  50. }
  51. document.getElementsByTagName('head')[0].appendChild(style);
  52. };
  53. /**
  54. * Define CommonJS module exports when not available
  55. */
  56. if (typeof exports !== 'undefined') {
  57. exports = vis;
  58. }
  59. if (typeof module !== 'undefined') {
  60. module.exports = vis;
  61. }
  62. /**
  63. * AMD module exports
  64. */
  65. if (typeof(define) === 'function') {
  66. define(function () {
  67. return vis;
  68. });
  69. }
  70. /**
  71. * Window exports
  72. */
  73. if (typeof window !== 'undefined') {
  74. // attach the module to the window, load as a regular javascript file
  75. window['vis'] = vis;
  76. }
  77. // create namespace
  78. var util = {};
  79. /**
  80. * Test whether given object is a number
  81. * @param {*} object
  82. * @return {Boolean} isNumber
  83. */
  84. util.isNumber = function isNumber(object) {
  85. return (object instanceof Number || typeof object == 'number');
  86. };
  87. /**
  88. * Test whether given object is a string
  89. * @param {*} object
  90. * @return {Boolean} isString
  91. */
  92. util.isString = function isString(object) {
  93. return (object instanceof String || typeof object == 'string');
  94. };
  95. /**
  96. * Test whether given object is a Date, or a String containing a Date
  97. * @param {Date | String} object
  98. * @return {Boolean} isDate
  99. */
  100. util.isDate = function isDate(object) {
  101. if (object instanceof Date) {
  102. return true;
  103. }
  104. else if (util.isString(object)) {
  105. // test whether this string contains a date
  106. var match = ASPDateRegex.exec(object);
  107. if (match) {
  108. return true;
  109. }
  110. else if (!isNaN(Date.parse(object))) {
  111. return true;
  112. }
  113. }
  114. return false;
  115. };
  116. /**
  117. * Test whether given object is an instance of google.visualization.DataTable
  118. * @param {*} object
  119. * @return {Boolean} isDataTable
  120. */
  121. util.isDataTable = function isDataTable(object) {
  122. return (typeof (google) !== 'undefined') &&
  123. (google.visualization) &&
  124. (google.visualization.DataTable) &&
  125. (object instanceof google.visualization.DataTable);
  126. };
  127. /**
  128. * Create a semi UUID
  129. * source: http://stackoverflow.com/a/105074/1262753
  130. * @return {String} uuid
  131. */
  132. util.randomUUID = function randomUUID () {
  133. var S4 = function () {
  134. return Math.floor(
  135. Math.random() * 0x10000 /* 65536 */
  136. ).toString(16);
  137. };
  138. return (
  139. S4() + S4() + '-' +
  140. S4() + '-' +
  141. S4() + '-' +
  142. S4() + '-' +
  143. S4() + S4() + S4()
  144. );
  145. };
  146. /**
  147. * Extend object a with the properties of object b
  148. * @param {Object} a
  149. * @param {Object} b
  150. * @return {Object} a
  151. */
  152. util.extend = function (a, b) {
  153. for (var prop in b) {
  154. if (b.hasOwnProperty(prop)) {
  155. a[prop] = b[prop];
  156. }
  157. }
  158. return a;
  159. };
  160. /**
  161. * Cast an object to another type
  162. * @param {Boolean | Number | String | Date | Null | undefined} object
  163. * @param {String | function | undefined} type Name of the type or a cast
  164. * function. Available types:
  165. * 'Boolean', 'Number', 'String',
  166. * 'Date', ISODate', 'ASPDate'.
  167. * @return {*} object
  168. * @throws Error
  169. */
  170. util.cast = function cast(object, type) {
  171. if (object === undefined) {
  172. return undefined;
  173. }
  174. if (object === null) {
  175. return null;
  176. }
  177. if (!type) {
  178. return object;
  179. }
  180. if (typeof type == 'function') {
  181. return type(object);
  182. }
  183. //noinspection FallthroughInSwitchStatementJS
  184. switch (type) {
  185. case 'boolean':
  186. case 'Boolean':
  187. return Boolean(object);
  188. case 'number':
  189. case 'Number':
  190. return Number(object);
  191. case 'string':
  192. case 'String':
  193. return String(object);
  194. case 'Date':
  195. if (util.isNumber(object)) {
  196. return new Date(object);
  197. }
  198. if (object instanceof Date) {
  199. return new Date(object.valueOf());
  200. }
  201. if (util.isString(object)) {
  202. // parse ASP.Net Date pattern,
  203. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  204. // code from http://momentjs.com/
  205. var match = ASPDateRegex.exec(object);
  206. if (match) {
  207. return new Date(Number(match[1]));
  208. }
  209. else {
  210. return moment(object).toDate(); // parse string
  211. }
  212. }
  213. else {
  214. throw new Error(
  215. 'Cannot cast object of type ' + util.getType(object) +
  216. ' to type Date');
  217. }
  218. case 'ISODate':
  219. if (object instanceof Date) {
  220. return object.toISOString();
  221. }
  222. else if (util.isNumber(object) || util.isString(object)) {
  223. return moment(object).toDate().toISOString();
  224. }
  225. else {
  226. throw new Error(
  227. 'Cannot cast object of type ' + util.getType(object) +
  228. ' to type ISODate');
  229. }
  230. case 'ASPDate':
  231. if (object instanceof Date) {
  232. return '/Date(' + object.valueOf() + ')/';
  233. }
  234. else if (util.isNumber(object) || util.isString(object)) {
  235. return '/Date(' + moment(object).valueOf() + ')/';
  236. }
  237. else {
  238. throw new Error(
  239. 'Cannot cast object of type ' + util.getType(object) +
  240. ' to type ASPDate');
  241. }
  242. default:
  243. throw new Error('Cannot cast object of type ' + util.getType(object) +
  244. ' to type "' + type + '"');
  245. }
  246. };
  247. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  248. /**
  249. * Get the type of an object, for example util.getType([]) returns 'Array'
  250. * @param {*} object
  251. * @return {String} type
  252. */
  253. util.getType = function getType(object) {
  254. var type = typeof object;
  255. if (type == 'object') {
  256. if (object == null) {
  257. return 'null';
  258. }
  259. if (object instanceof Boolean) {
  260. return 'Boolean';
  261. }
  262. if (object instanceof Number) {
  263. return 'Number';
  264. }
  265. if (object instanceof String) {
  266. return 'String';
  267. }
  268. if (object instanceof Array) {
  269. return 'Array';
  270. }
  271. if (object instanceof Date) {
  272. return 'Date';
  273. }
  274. return 'Object';
  275. }
  276. else if (type == 'number') {
  277. return 'Number';
  278. }
  279. else if (type == 'boolean') {
  280. return 'Boolean';
  281. }
  282. else if (type == 'string') {
  283. return 'String';
  284. }
  285. return type;
  286. };
  287. /**
  288. * Retrieve the absolute left value of a DOM element
  289. * @param {Element} elem A dom element, for example a div
  290. * @return {number} left The absolute left position of this element
  291. * in the browser page.
  292. */
  293. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  294. var doc = document.documentElement;
  295. var body = document.body;
  296. var left = elem.offsetLeft;
  297. var e = elem.offsetParent;
  298. while (e != null && e != body && e != doc) {
  299. left += e.offsetLeft;
  300. left -= e.scrollLeft;
  301. e = e.offsetParent;
  302. }
  303. return left;
  304. };
  305. /**
  306. * Retrieve the absolute top value of a DOM element
  307. * @param {Element} elem A dom element, for example a div
  308. * @return {number} top The absolute top position of this element
  309. * in the browser page.
  310. */
  311. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  312. var doc = document.documentElement;
  313. var body = document.body;
  314. var top = elem.offsetTop;
  315. var e = elem.offsetParent;
  316. while (e != null && e != body && e != doc) {
  317. top += e.offsetTop;
  318. top -= e.scrollTop;
  319. e = e.offsetParent;
  320. }
  321. return top;
  322. };
  323. /**
  324. * Get the absolute, vertical mouse position from an event.
  325. * @param {Event} event
  326. * @return {Number} pageY
  327. */
  328. util.getPageY = function getPageY (event) {
  329. if ('pageY' in event) {
  330. return event.pageY;
  331. }
  332. else {
  333. var clientY;
  334. if (('targetTouches' in event) && event.targetTouches.length) {
  335. clientY = event.targetTouches[0].clientY;
  336. }
  337. else {
  338. clientY = event.clientY;
  339. }
  340. var doc = document.documentElement;
  341. var body = document.body;
  342. return clientY +
  343. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  344. ( doc && doc.clientTop || body && body.clientTop || 0 );
  345. }
  346. };
  347. /**
  348. * Get the absolute, horizontal mouse position from an event.
  349. * @param {Event} event
  350. * @return {Number} pageX
  351. */
  352. util.getPageX = function getPageX (event) {
  353. if ('pageY' in event) {
  354. return event.pageX;
  355. }
  356. else {
  357. var clientX;
  358. if (('targetTouches' in event) && event.targetTouches.length) {
  359. clientX = event.targetTouches[0].clientX;
  360. }
  361. else {
  362. clientX = event.clientX;
  363. }
  364. var doc = document.documentElement;
  365. var body = document.body;
  366. return clientX +
  367. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  368. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  369. }
  370. };
  371. /**
  372. * add a className to the given elements style
  373. * @param {Element} elem
  374. * @param {String} className
  375. */
  376. util.addClassName = function addClassName(elem, className) {
  377. var classes = elem.className.split(' ');
  378. if (classes.indexOf(className) == -1) {
  379. classes.push(className); // add the class to the array
  380. elem.className = classes.join(' ');
  381. }
  382. };
  383. /**
  384. * add a className to the given elements style
  385. * @param {Element} elem
  386. * @param {String} className
  387. */
  388. util.removeClassName = function removeClassname(elem, className) {
  389. var classes = elem.className.split(' ');
  390. var index = classes.indexOf(className);
  391. if (index != -1) {
  392. classes.splice(index, 1); // remove the class from the array
  393. elem.className = classes.join(' ');
  394. }
  395. };
  396. /**
  397. * For each method for both arrays and objects.
  398. * In case of an array, the built-in Array.forEach() is applied.
  399. * In case of an Object, the method loops over all properties of the object.
  400. * @param {Object | Array} object An Object or Array
  401. * @param {function} callback Callback method, called for each item in
  402. * the object or array with three parameters:
  403. * callback(value, index, object)
  404. */
  405. util.forEach = function forEach (object, callback) {
  406. if (object instanceof Array) {
  407. // array
  408. object.forEach(callback);
  409. }
  410. else {
  411. // object
  412. for (var key in object) {
  413. if (object.hasOwnProperty(key)) {
  414. callback(object[key], key, object);
  415. }
  416. }
  417. }
  418. };
  419. /**
  420. * Update a property in an object
  421. * @param {Object} object
  422. * @param {String} key
  423. * @param {*} value
  424. * @return {Boolean} changed
  425. */
  426. util.updateProperty = function updateProp (object, key, value) {
  427. if (object[key] !== value) {
  428. object[key] = value;
  429. return true;
  430. }
  431. else {
  432. return false;
  433. }
  434. };
  435. /**
  436. * Add and event listener. Works for all browsers
  437. * @param {Element} element An html element
  438. * @param {string} action The action, for example "click",
  439. * without the prefix "on"
  440. * @param {function} listener The callback function to be executed
  441. * @param {boolean} [useCapture]
  442. */
  443. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  444. if (element.addEventListener) {
  445. if (useCapture === undefined)
  446. useCapture = false;
  447. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  448. action = "DOMMouseScroll"; // For Firefox
  449. }
  450. element.addEventListener(action, listener, useCapture);
  451. } else {
  452. element.attachEvent("on" + action, listener); // IE browsers
  453. }
  454. };
  455. /**
  456. * Remove an event listener from an element
  457. * @param {Element} element An html dom element
  458. * @param {string} action The name of the event, for example "mousedown"
  459. * @param {function} listener The listener function
  460. * @param {boolean} [useCapture]
  461. */
  462. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  463. if (element.removeEventListener) {
  464. // non-IE browsers
  465. if (useCapture === undefined)
  466. useCapture = false;
  467. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  468. action = "DOMMouseScroll"; // For Firefox
  469. }
  470. element.removeEventListener(action, listener, useCapture);
  471. } else {
  472. // IE browsers
  473. element.detachEvent("on" + action, listener);
  474. }
  475. };
  476. /**
  477. * Get HTML element which is the target of the event
  478. * @param {Event} event
  479. * @return {Element} target element
  480. */
  481. util.getTarget = function getTarget(event) {
  482. // code from http://www.quirksmode.org/js/events_properties.html
  483. if (!event) {
  484. event = window.event;
  485. }
  486. var target;
  487. if (event.target) {
  488. target = event.target;
  489. }
  490. else if (event.srcElement) {
  491. target = event.srcElement;
  492. }
  493. if (target.nodeType != undefined && target.nodeType == 3) {
  494. // defeat Safari bug
  495. target = target.parentNode;
  496. }
  497. return target;
  498. };
  499. /**
  500. * Stop event propagation
  501. */
  502. util.stopPropagation = function stopPropagation(event) {
  503. if (!event)
  504. event = window.event;
  505. if (event.stopPropagation) {
  506. event.stopPropagation(); // non-IE browsers
  507. }
  508. else {
  509. event.cancelBubble = true; // IE browsers
  510. }
  511. };
  512. /**
  513. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  514. */
  515. util.preventDefault = function preventDefault (event) {
  516. if (!event)
  517. event = window.event;
  518. if (event.preventDefault) {
  519. event.preventDefault(); // non-IE browsers
  520. }
  521. else {
  522. event.returnValue = false; // IE browsers
  523. }
  524. };
  525. util.option = {};
  526. /**
  527. * Cast a value as boolean
  528. * @param {Boolean | function | undefined} value
  529. * @param {Boolean} [defaultValue]
  530. * @returns {Boolean} bool
  531. */
  532. util.option.asBoolean = function (value, defaultValue) {
  533. if (typeof value == 'function') {
  534. value = value();
  535. }
  536. if (value != null) {
  537. return (value != false);
  538. }
  539. return defaultValue || null;
  540. };
  541. /**
  542. * Cast a value as string
  543. * @param {String | function | undefined} value
  544. * @param {String} [defaultValue]
  545. * @returns {String} str
  546. */
  547. util.option.asString = function (value, defaultValue) {
  548. if (typeof value == 'function') {
  549. value = value();
  550. }
  551. if (value != null) {
  552. return String(value);
  553. }
  554. return defaultValue || null;
  555. };
  556. /**
  557. * Cast a size or location in pixels or a percentage
  558. * @param {String | Number | function | undefined} value
  559. * @param {String} [defaultValue]
  560. * @returns {String} size
  561. */
  562. util.option.asSize = function (value, defaultValue) {
  563. if (typeof value == 'function') {
  564. value = value();
  565. }
  566. if (util.isString(value)) {
  567. return value;
  568. }
  569. else if (util.isNumber(value)) {
  570. return value + 'px';
  571. }
  572. else {
  573. return defaultValue || null;
  574. }
  575. };
  576. /**
  577. * Cast a value as DOM element
  578. * @param {HTMLElement | function | undefined} value
  579. * @param {HTMLElement} [defaultValue]
  580. * @returns {HTMLElement | null} dom
  581. */
  582. util.option.asElement = function (value, defaultValue) {
  583. if (typeof value == 'function') {
  584. value = value();
  585. }
  586. return value || defaultValue || null;
  587. };
  588. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  589. // it here in that case.
  590. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  591. if(!Array.prototype.indexOf) {
  592. Array.prototype.indexOf = function(obj){
  593. for(var i = 0; i < this.length; i++){
  594. if(this[i] == obj){
  595. return i;
  596. }
  597. }
  598. return -1;
  599. };
  600. try {
  601. console.log("Warning: Ancient browser detected. Please update your browser");
  602. }
  603. catch (err) {
  604. }
  605. }
  606. // Internet Explorer 8 and older does not support Array.forEach, so we define
  607. // it here in that case.
  608. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  609. if (!Array.prototype.forEach) {
  610. Array.prototype.forEach = function(fn, scope) {
  611. for(var i = 0, len = this.length; i < len; ++i) {
  612. fn.call(scope || this, this[i], i, this);
  613. }
  614. }
  615. }
  616. // Internet Explorer 8 and older does not support Array.map, so we define it
  617. // here in that case.
  618. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  619. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  620. // Reference: http://es5.github.com/#x15.4.4.19
  621. if (!Array.prototype.map) {
  622. Array.prototype.map = function(callback, thisArg) {
  623. var T, A, k;
  624. if (this == null) {
  625. throw new TypeError(" this is null or not defined");
  626. }
  627. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  628. var O = Object(this);
  629. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  630. // 3. Let len be ToUint32(lenValue).
  631. var len = O.length >>> 0;
  632. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  633. // See: http://es5.github.com/#x9.11
  634. if (typeof callback !== "function") {
  635. throw new TypeError(callback + " is not a function");
  636. }
  637. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  638. if (thisArg) {
  639. T = thisArg;
  640. }
  641. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  642. // the standard built-in constructor with that name and len is the value of len.
  643. A = new Array(len);
  644. // 7. Let k be 0
  645. k = 0;
  646. // 8. Repeat, while k < len
  647. while(k < len) {
  648. var kValue, mappedValue;
  649. // a. Let Pk be ToString(k).
  650. // This is implicit for LHS operands of the in operator
  651. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  652. // This step can be combined with c
  653. // c. If kPresent is true, then
  654. if (k in O) {
  655. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  656. kValue = O[ k ];
  657. // ii. Let mappedValue be the result of calling the Call internal method of callback
  658. // with T as the this value and argument list containing kValue, k, and O.
  659. mappedValue = callback.call(T, kValue, k, O);
  660. // iii. Call the DefineOwnProperty internal method of A with arguments
  661. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  662. // and false.
  663. // In browsers that support Object.defineProperty, use the following:
  664. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  665. // For best browser support, use the following:
  666. A[ k ] = mappedValue;
  667. }
  668. // d. Increase k by 1.
  669. k++;
  670. }
  671. // 9. return A
  672. return A;
  673. };
  674. }
  675. // Internet Explorer 8 and older does not support Array.filter, so we define it
  676. // here in that case.
  677. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  678. if (!Array.prototype.filter) {
  679. Array.prototype.filter = function(fun /*, thisp */) {
  680. "use strict";
  681. if (this == null) {
  682. throw new TypeError();
  683. }
  684. var t = Object(this);
  685. var len = t.length >>> 0;
  686. if (typeof fun != "function") {
  687. throw new TypeError();
  688. }
  689. var res = [];
  690. var thisp = arguments[1];
  691. for (var i = 0; i < len; i++) {
  692. if (i in t) {
  693. var val = t[i]; // in case fun mutates this
  694. if (fun.call(thisp, val, i, t))
  695. res.push(val);
  696. }
  697. }
  698. return res;
  699. };
  700. }
  701. // Internet Explorer 8 and older does not support Object.keys, so we define it
  702. // here in that case.
  703. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  704. if (!Object.keys) {
  705. Object.keys = (function () {
  706. var hasOwnProperty = Object.prototype.hasOwnProperty,
  707. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  708. dontEnums = [
  709. 'toString',
  710. 'toLocaleString',
  711. 'valueOf',
  712. 'hasOwnProperty',
  713. 'isPrototypeOf',
  714. 'propertyIsEnumerable',
  715. 'constructor'
  716. ],
  717. dontEnumsLength = dontEnums.length;
  718. return function (obj) {
  719. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  720. throw new TypeError('Object.keys called on non-object');
  721. }
  722. var result = [];
  723. for (var prop in obj) {
  724. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  725. }
  726. if (hasDontEnumBug) {
  727. for (var i=0; i < dontEnumsLength; i++) {
  728. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  729. }
  730. }
  731. return result;
  732. }
  733. })()
  734. }
  735. // Internet Explorer 8 and older does not support Array.isArray,
  736. // so we define it here in that case.
  737. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  738. if(!Array.isArray) {
  739. Array.isArray = function (vArg) {
  740. return Object.prototype.toString.call(vArg) === "[object Array]";
  741. };
  742. }
  743. // export
  744. vis.util = util;
  745. /**
  746. * Event listener (singleton)
  747. */
  748. var events = {
  749. 'listeners': [],
  750. /**
  751. * Find a single listener by its object
  752. * @param {Object} object
  753. * @return {Number} index -1 when not found
  754. */
  755. 'indexOf': function (object) {
  756. var listeners = this.listeners;
  757. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  758. var listener = listeners[i];
  759. if (listener && listener.object == object) {
  760. return i;
  761. }
  762. }
  763. return -1;
  764. },
  765. /**
  766. * Add an event listener
  767. * @param {Object} object
  768. * @param {String} event The name of an event, for example 'select'
  769. * @param {function} callback The callback method, called when the
  770. * event takes place
  771. */
  772. 'addListener': function (object, event, callback) {
  773. var index = this.indexOf(object);
  774. var listener = this.listeners[index];
  775. if (!listener) {
  776. listener = {
  777. 'object': object,
  778. 'events': {}
  779. };
  780. this.listeners.push(listener);
  781. }
  782. var callbacks = listener.events[event];
  783. if (!callbacks) {
  784. callbacks = [];
  785. listener.events[event] = callbacks;
  786. }
  787. // add the callback if it does not yet exist
  788. if (callbacks.indexOf(callback) == -1) {
  789. callbacks.push(callback);
  790. }
  791. },
  792. /**
  793. * Remove an event listener
  794. * @param {Object} object
  795. * @param {String} event The name of an event, for example 'select'
  796. * @param {function} callback The registered callback method
  797. */
  798. 'removeListener': function (object, event, callback) {
  799. var index = this.indexOf(object);
  800. var listener = this.listeners[index];
  801. if (listener) {
  802. var callbacks = listener.events[event];
  803. if (callbacks) {
  804. index = callbacks.indexOf(callback);
  805. if (index != -1) {
  806. callbacks.splice(index, 1);
  807. }
  808. // remove the array when empty
  809. if (callbacks.length == 0) {
  810. delete listener.events[event];
  811. }
  812. }
  813. // count the number of registered events. remove listener when empty
  814. var count = 0;
  815. var events = listener.events;
  816. for (var e in events) {
  817. if (events.hasOwnProperty(e)) {
  818. count++;
  819. }
  820. }
  821. if (count == 0) {
  822. delete this.listeners[index];
  823. }
  824. }
  825. },
  826. /**
  827. * Remove all registered event listeners
  828. */
  829. 'removeAllListeners': function () {
  830. this.listeners = [];
  831. },
  832. /**
  833. * Trigger an event. All registered event handlers will be called
  834. * @param {Object} object
  835. * @param {String} event
  836. * @param {Object} properties (optional)
  837. */
  838. 'trigger': function (object, event, properties) {
  839. var index = this.indexOf(object);
  840. var listener = this.listeners[index];
  841. if (listener) {
  842. var callbacks = listener.events[event];
  843. if (callbacks) {
  844. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  845. callbacks[i](properties);
  846. }
  847. }
  848. }
  849. }
  850. };
  851. // exports
  852. vis.events = events;
  853. /**
  854. * @constructor TimeStep
  855. * The class TimeStep is an iterator for dates. You provide a start date and an
  856. * end date. The class itself determines the best scale (step size) based on the
  857. * provided start Date, end Date, and minimumStep.
  858. *
  859. * If minimumStep is provided, the step size is chosen as close as possible
  860. * to the minimumStep but larger than minimumStep. If minimumStep is not
  861. * provided, the scale is set to 1 DAY.
  862. * The minimumStep should correspond with the onscreen size of about 6 characters
  863. *
  864. * Alternatively, you can set a scale by hand.
  865. * After creation, you can initialize the class by executing first(). Then you
  866. * can iterate from the start date to the end date via next(). You can check if
  867. * the end date is reached with the function hasNext(). After each step, you can
  868. * retrieve the current date via getCurrent().
  869. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  870. * days, to years.
  871. *
  872. * Version: 1.2
  873. *
  874. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  875. * or new Date(2010, 9, 21, 23, 45, 00)
  876. * @param {Date} [end] The end date
  877. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  878. */
  879. TimeStep = function(start, end, minimumStep) {
  880. // variables
  881. this.current = new Date();
  882. this._start = new Date();
  883. this._end = new Date();
  884. this.autoScale = true;
  885. this.scale = TimeStep.SCALE.DAY;
  886. this.step = 1;
  887. // initialize the range
  888. this.setRange(start, end, minimumStep);
  889. };
  890. /// enum scale
  891. TimeStep.SCALE = {
  892. MILLISECOND: 1,
  893. SECOND: 2,
  894. MINUTE: 3,
  895. HOUR: 4,
  896. DAY: 5,
  897. WEEKDAY: 6,
  898. MONTH: 7,
  899. YEAR: 8
  900. };
  901. /**
  902. * Set a new range
  903. * If minimumStep is provided, the step size is chosen as close as possible
  904. * to the minimumStep but larger than minimumStep. If minimumStep is not
  905. * provided, the scale is set to 1 DAY.
  906. * The minimumStep should correspond with the onscreen size of about 6 characters
  907. * @param {Date} start The start date and time.
  908. * @param {Date} end The end date and time.
  909. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  910. */
  911. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  912. if (!(start instanceof Date) || !(end instanceof Date)) {
  913. //throw "No legal start or end date in method setRange";
  914. return;
  915. }
  916. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  917. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  918. if (this.autoScale) {
  919. this.setMinimumStep(minimumStep);
  920. }
  921. };
  922. /**
  923. * Set the range iterator to the start date.
  924. */
  925. TimeStep.prototype.first = function() {
  926. this.current = new Date(this._start.valueOf());
  927. this.roundToMinor();
  928. };
  929. /**
  930. * Round the current date to the first minor date value
  931. * This must be executed once when the current date is set to start Date
  932. */
  933. TimeStep.prototype.roundToMinor = function() {
  934. // round to floor
  935. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  936. //noinspection FallthroughInSwitchStatementJS
  937. switch (this.scale) {
  938. case TimeStep.SCALE.YEAR:
  939. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  940. this.current.setMonth(0);
  941. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  942. case TimeStep.SCALE.DAY: // intentional fall through
  943. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  944. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  945. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  946. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  947. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  948. }
  949. if (this.step != 1) {
  950. // round down to the first minor value that is a multiple of the current step size
  951. switch (this.scale) {
  952. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  953. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  954. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  955. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  956. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  957. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  958. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  959. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  960. default: break;
  961. }
  962. }
  963. };
  964. /**
  965. * Check if the there is a next step
  966. * @return {boolean} true if the current date has not passed the end date
  967. */
  968. TimeStep.prototype.hasNext = function () {
  969. return (this.current.valueOf() <= this._end.valueOf());
  970. };
  971. /**
  972. * Do the next step
  973. */
  974. TimeStep.prototype.next = function() {
  975. var prev = this.current.valueOf();
  976. // Two cases, needed to prevent issues with switching daylight savings
  977. // (end of March and end of October)
  978. if (this.current.getMonth() < 6) {
  979. switch (this.scale) {
  980. case TimeStep.SCALE.MILLISECOND:
  981. this.current = new Date(this.current.valueOf() + this.step); break;
  982. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  983. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  984. case TimeStep.SCALE.HOUR:
  985. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  986. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  987. var h = this.current.getHours();
  988. this.current.setHours(h - (h % this.step));
  989. break;
  990. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  991. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  992. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  993. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  994. default: break;
  995. }
  996. }
  997. else {
  998. switch (this.scale) {
  999. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  1000. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  1001. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  1002. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  1003. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1004. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  1005. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  1006. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  1007. default: break;
  1008. }
  1009. }
  1010. if (this.step != 1) {
  1011. // round down to the correct major value
  1012. switch (this.scale) {
  1013. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  1014. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  1015. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  1016. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  1017. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1018. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  1019. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  1020. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  1021. default: break;
  1022. }
  1023. }
  1024. // safety mechanism: if current time is still unchanged, move to the end
  1025. if (this.current.valueOf() == prev) {
  1026. this.current = new Date(this._end.valueOf());
  1027. }
  1028. };
  1029. /**
  1030. * Get the current datetime
  1031. * @return {Date} current The current date
  1032. */
  1033. TimeStep.prototype.getCurrent = function() {
  1034. return this.current;
  1035. };
  1036. /**
  1037. * Set a custom scale. Autoscaling will be disabled.
  1038. * For example setScale(SCALE.MINUTES, 5) will result
  1039. * in minor steps of 5 minutes, and major steps of an hour.
  1040. *
  1041. * @param {TimeStep.SCALE} newScale
  1042. * A scale. Choose from SCALE.MILLISECOND,
  1043. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  1044. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  1045. * SCALE.YEAR.
  1046. * @param {Number} newStep A step size, by default 1. Choose for
  1047. * example 1, 2, 5, or 10.
  1048. */
  1049. TimeStep.prototype.setScale = function(newScale, newStep) {
  1050. this.scale = newScale;
  1051. if (newStep > 0) {
  1052. this.step = newStep;
  1053. }
  1054. this.autoScale = false;
  1055. };
  1056. /**
  1057. * Enable or disable autoscaling
  1058. * @param {boolean} enable If true, autoascaling is set true
  1059. */
  1060. TimeStep.prototype.setAutoScale = function (enable) {
  1061. this.autoScale = enable;
  1062. };
  1063. /**
  1064. * Automatically determine the scale that bests fits the provided minimum step
  1065. * @param {Number} minimumStep The minimum step size in milliseconds
  1066. */
  1067. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  1068. if (minimumStep == undefined) {
  1069. return;
  1070. }
  1071. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  1072. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  1073. var stepDay = (1000 * 60 * 60 * 24);
  1074. var stepHour = (1000 * 60 * 60);
  1075. var stepMinute = (1000 * 60);
  1076. var stepSecond = (1000);
  1077. var stepMillisecond= (1);
  1078. // find the smallest step that is larger than the provided minimumStep
  1079. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  1080. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  1081. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  1082. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  1083. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  1084. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  1085. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  1086. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  1087. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  1088. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  1089. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  1090. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  1091. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  1092. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  1093. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  1094. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  1095. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  1096. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  1097. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  1098. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  1099. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  1100. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  1101. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  1102. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  1103. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  1104. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  1105. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  1106. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  1107. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  1108. };
  1109. /**
  1110. * Snap a date to a rounded value. The snap intervals are dependent on the
  1111. * current scale and step.
  1112. * @param {Date} date the date to be snapped
  1113. */
  1114. TimeStep.prototype.snap = function(date) {
  1115. if (this.scale == TimeStep.SCALE.YEAR) {
  1116. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  1117. date.setFullYear(Math.round(year / this.step) * this.step);
  1118. date.setMonth(0);
  1119. date.setDate(0);
  1120. date.setHours(0);
  1121. date.setMinutes(0);
  1122. date.setSeconds(0);
  1123. date.setMilliseconds(0);
  1124. }
  1125. else if (this.scale == TimeStep.SCALE.MONTH) {
  1126. if (date.getDate() > 15) {
  1127. date.setDate(1);
  1128. date.setMonth(date.getMonth() + 1);
  1129. // important: first set Date to 1, after that change the month.
  1130. }
  1131. else {
  1132. date.setDate(1);
  1133. }
  1134. date.setHours(0);
  1135. date.setMinutes(0);
  1136. date.setSeconds(0);
  1137. date.setMilliseconds(0);
  1138. }
  1139. else if (this.scale == TimeStep.SCALE.DAY ||
  1140. this.scale == TimeStep.SCALE.WEEKDAY) {
  1141. //noinspection FallthroughInSwitchStatementJS
  1142. switch (this.step) {
  1143. case 5:
  1144. case 2:
  1145. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  1146. default:
  1147. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  1148. }
  1149. date.setMinutes(0);
  1150. date.setSeconds(0);
  1151. date.setMilliseconds(0);
  1152. }
  1153. else if (this.scale == TimeStep.SCALE.HOUR) {
  1154. switch (this.step) {
  1155. case 4:
  1156. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  1157. default:
  1158. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  1159. }
  1160. date.setSeconds(0);
  1161. date.setMilliseconds(0);
  1162. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  1163. //noinspection FallthroughInSwitchStatementJS
  1164. switch (this.step) {
  1165. case 15:
  1166. case 10:
  1167. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  1168. date.setSeconds(0);
  1169. break;
  1170. case 5:
  1171. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  1172. default:
  1173. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  1174. }
  1175. date.setMilliseconds(0);
  1176. }
  1177. else if (this.scale == TimeStep.SCALE.SECOND) {
  1178. //noinspection FallthroughInSwitchStatementJS
  1179. switch (this.step) {
  1180. case 15:
  1181. case 10:
  1182. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  1183. date.setMilliseconds(0);
  1184. break;
  1185. case 5:
  1186. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  1187. default:
  1188. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  1189. }
  1190. }
  1191. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  1192. var step = this.step > 5 ? this.step / 2 : 1;
  1193. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  1194. }
  1195. };
  1196. /**
  1197. * Check if the current value is a major value (for example when the step
  1198. * is DAY, a major value is each first day of the MONTH)
  1199. * @return {boolean} true if current date is major, else false.
  1200. */
  1201. TimeStep.prototype.isMajor = function() {
  1202. switch (this.scale) {
  1203. case TimeStep.SCALE.MILLISECOND:
  1204. return (this.current.getMilliseconds() == 0);
  1205. case TimeStep.SCALE.SECOND:
  1206. return (this.current.getSeconds() == 0);
  1207. case TimeStep.SCALE.MINUTE:
  1208. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  1209. // Note: this is no bug. Major label is equal for both minute and hour scale
  1210. case TimeStep.SCALE.HOUR:
  1211. return (this.current.getHours() == 0);
  1212. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1213. case TimeStep.SCALE.DAY:
  1214. return (this.current.getDate() == 1);
  1215. case TimeStep.SCALE.MONTH:
  1216. return (this.current.getMonth() == 0);
  1217. case TimeStep.SCALE.YEAR:
  1218. return false;
  1219. default:
  1220. return false;
  1221. }
  1222. };
  1223. /**
  1224. * Returns formatted text for the minor axislabel, depending on the current
  1225. * date and the scale. For example when scale is MINUTE, the current time is
  1226. * formatted as "hh:mm".
  1227. * @param {Date} [date] custom date. if not provided, current date is taken
  1228. */
  1229. TimeStep.prototype.getLabelMinor = function(date) {
  1230. if (date == undefined) {
  1231. date = this.current;
  1232. }
  1233. switch (this.scale) {
  1234. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  1235. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  1236. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  1237. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  1238. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  1239. case TimeStep.SCALE.DAY: return moment(date).format('D');
  1240. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  1241. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  1242. default: return '';
  1243. }
  1244. };
  1245. /**
  1246. * Returns formatted text for the major axislabel, depending on the current
  1247. * date and the scale. For example when scale is MINUTE, the major scale is
  1248. * hours, and the hour will be formatted as "hh".
  1249. * @param {Date} [date] custom date. if not provided, current date is taken
  1250. */
  1251. TimeStep.prototype.getLabelMajor = function(date) {
  1252. if (date == undefined) {
  1253. date = this.current;
  1254. }
  1255. //noinspection FallthroughInSwitchStatementJS
  1256. switch (this.scale) {
  1257. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  1258. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  1259. case TimeStep.SCALE.MINUTE:
  1260. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  1261. case TimeStep.SCALE.WEEKDAY:
  1262. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  1263. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  1264. case TimeStep.SCALE.YEAR: return '';
  1265. default: return '';
  1266. }
  1267. };
  1268. // export
  1269. vis.TimeStep = TimeStep;
  1270. /**
  1271. * DataSet
  1272. *
  1273. * Usage:
  1274. * var dataSet = new DataSet({
  1275. * fieldId: '_id',
  1276. * fieldTypes: {
  1277. * // ...
  1278. * }
  1279. * });
  1280. *
  1281. * dataSet.add(item);
  1282. * dataSet.add(data);
  1283. * dataSet.update(item);
  1284. * dataSet.update(data);
  1285. * dataSet.remove(id);
  1286. * dataSet.remove(ids);
  1287. * var data = dataSet.get();
  1288. * var data = dataSet.get(id);
  1289. * var data = dataSet.get(ids);
  1290. * var data = dataSet.get(ids, options, data);
  1291. * dataSet.clear();
  1292. *
  1293. * A data set can:
  1294. * - add/remove/update data
  1295. * - gives triggers upon changes in the data
  1296. * - can import/export data in various data formats
  1297. * @param {Object} [options] Available options:
  1298. * {String} fieldId Field name of the id in the
  1299. * items, 'id' by default.
  1300. * {Object.<String, String} fieldTypes
  1301. * A map with field names as key,
  1302. * and the field type as value.
  1303. */
  1304. function DataSet (options) {
  1305. var me = this;
  1306. this.options = options || {};
  1307. this.data = {}; // map with data indexed by id
  1308. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1309. this.fieldTypes = {}; // field types by field name
  1310. if (this.options.fieldTypes) {
  1311. util.forEach(this.options.fieldTypes, function (value, field) {
  1312. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1313. me.fieldTypes[field] = 'Date';
  1314. }
  1315. else {
  1316. me.fieldTypes[field] = value;
  1317. }
  1318. });
  1319. }
  1320. // event subscribers
  1321. this.subscribers = {};
  1322. this.internalIds = {}; // internally generated id's
  1323. }
  1324. /**
  1325. * Subscribe to an event, add an event listener
  1326. * @param {String} event Event name. Available events: 'put', 'update',
  1327. * 'remove'
  1328. * @param {function} callback Callback method. Called with three parameters:
  1329. * {String} event
  1330. * {Object | null} params
  1331. * {String} senderId
  1332. * @param {String} [id] Optional id for the sender, used to filter
  1333. * events triggered by the sender itself.
  1334. */
  1335. DataSet.prototype.subscribe = function (event, callback, id) {
  1336. var subscribers = this.subscribers[event];
  1337. if (!subscribers) {
  1338. subscribers = [];
  1339. this.subscribers[event] = subscribers;
  1340. }
  1341. subscribers.push({
  1342. id: id ? String(id) : null,
  1343. callback: callback
  1344. });
  1345. };
  1346. /**
  1347. * Unsubscribe from an event, remove an event listener
  1348. * @param {String} event
  1349. * @param {function} callback
  1350. */
  1351. DataSet.prototype.unsubscribe = function (event, callback) {
  1352. var subscribers = this.subscribers[event];
  1353. if (subscribers) {
  1354. this.subscribers[event] = subscribers.filter(function (listener) {
  1355. return (listener.callback != callback);
  1356. });
  1357. }
  1358. };
  1359. /**
  1360. * Trigger an event
  1361. * @param {String} event
  1362. * @param {Object | null} params
  1363. * @param {String} [senderId] Optional id of the sender. The event will
  1364. * be triggered for all subscribers except the
  1365. * sender itself.
  1366. * @private
  1367. */
  1368. DataSet.prototype._trigger = function (event, params, senderId) {
  1369. if (event == '*') {
  1370. throw new Error('Cannot trigger event *');
  1371. }
  1372. var subscribers = [];
  1373. if (event in this.subscribers) {
  1374. subscribers = subscribers.concat(this.subscribers[event]);
  1375. }
  1376. if ('*' in this.subscribers) {
  1377. subscribers = subscribers.concat(this.subscribers['*']);
  1378. }
  1379. subscribers.forEach(function (listener) {
  1380. if (listener.id != senderId && listener.callback) {
  1381. listener.callback(event, params, senderId || null);
  1382. }
  1383. });
  1384. };
  1385. /**
  1386. * Add data. Existing items with the same id will be overwritten.
  1387. * @param {Object | Array | DataTable} data
  1388. * @param {String} [senderId] Optional sender id, used to trigger events for
  1389. * all but this sender's event subscribers.
  1390. */
  1391. DataSet.prototype.add = function (data, senderId) {
  1392. var items = [],
  1393. id,
  1394. me = this;
  1395. if (data instanceof Array) {
  1396. // Array
  1397. data.forEach(function (item) {
  1398. var id = me._addItem(item);
  1399. items.push(id);
  1400. });
  1401. }
  1402. else if (util.isDataTable(data)) {
  1403. // Google DataTable
  1404. var columns = this._getColumnNames(data);
  1405. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1406. var item = {};
  1407. columns.forEach(function (field, col) {
  1408. item[field] = data.getValue(row, col);
  1409. });
  1410. id = me._addItem(item);
  1411. items.push(id);
  1412. }
  1413. }
  1414. else if (data instanceof Object) {
  1415. // Single item
  1416. id = me._addItem(data);
  1417. items.push(id);
  1418. }
  1419. else {
  1420. throw new Error('Unknown dataType');
  1421. }
  1422. this._trigger('add', {items: items}, senderId);
  1423. };
  1424. /**
  1425. * Update existing items. Items with the same id will be merged
  1426. * @param {Object | Array | DataTable} data
  1427. * @param {String} [senderId] Optional sender id, used to trigger events for
  1428. * all but this sender's event subscribers.
  1429. */
  1430. DataSet.prototype.update = function (data, senderId) {
  1431. var items = [],
  1432. id,
  1433. me = this;
  1434. if (data instanceof Array) {
  1435. // Array
  1436. data.forEach(function (item) {
  1437. var id = me._updateItem(item);
  1438. items.push(id);
  1439. });
  1440. }
  1441. else if (util.isDataTable(data)) {
  1442. // Google DataTable
  1443. var columns = this._getColumnNames(data);
  1444. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1445. var item = {};
  1446. columns.forEach(function (field, col) {
  1447. item[field] = data.getValue(row, col);
  1448. });
  1449. id = me._updateItem(item);
  1450. items.push(id);
  1451. }
  1452. }
  1453. else if (data instanceof Object) {
  1454. // Single item
  1455. id = me._updateItem(data);
  1456. items.push(id);
  1457. }
  1458. else {
  1459. throw new Error('Unknown dataType');
  1460. }
  1461. this._trigger('update', {items: items}, senderId);
  1462. };
  1463. /**
  1464. * Get a data item or multiple items
  1465. * @param {String | Number | Array | Object} [ids] Id of a single item, or an
  1466. * array with multiple id's, or
  1467. * undefined or an Object with options
  1468. * to retrieve all data.
  1469. * @param {Object} [options] Available options:
  1470. * {String} [type]
  1471. * 'DataTable' or 'Array' (default)
  1472. * {Object.<String, String>} [fieldTypes]
  1473. * {String[]} [fields] filter fields
  1474. * @param {Array | DataTable} [data] If provided, items will be appended
  1475. * to this array or table. Required
  1476. * in case of Google DataTable
  1477. * @return {Array | Object | DataTable | null} data
  1478. * @throws Error
  1479. */
  1480. DataSet.prototype.get = function (ids, options, data) {
  1481. var me = this;
  1482. // shift arguments when first argument contains the options
  1483. if (util.getType(ids) == 'Object') {
  1484. data = options;
  1485. options = ids;
  1486. ids = undefined;
  1487. }
  1488. // merge field types
  1489. var fieldTypes = {};
  1490. if (this.options && this.options.fieldTypes) {
  1491. util.forEach(this.options.fieldTypes, function (value, field) {
  1492. fieldTypes[field] = value;
  1493. });
  1494. }
  1495. if (options && options.fieldTypes) {
  1496. util.forEach(options.fieldTypes, function (value, field) {
  1497. fieldTypes[field] = value;
  1498. });
  1499. }
  1500. var fields = options ? options.fields : undefined;
  1501. // determine the return type
  1502. var type;
  1503. if (options && options.type) {
  1504. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1505. if (data && (type != util.getType(data))) {
  1506. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1507. 'does not correspond with specified options.type (' + options.type + ')');
  1508. }
  1509. if (type == 'DataTable' && !util.isDataTable(data)) {
  1510. throw new Error('Parameter "data" must be a DataTable ' +
  1511. 'when options.type is "DataTable"');
  1512. }
  1513. }
  1514. else if (data) {
  1515. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1516. }
  1517. else {
  1518. type = 'Array';
  1519. }
  1520. if (type == 'DataTable') {
  1521. // return a Google DataTable
  1522. var columns = this._getColumnNames(data);
  1523. if (ids == undefined) {
  1524. // return all data
  1525. util.forEach(this.data, function (item) {
  1526. me._appendRow(data, columns, me._castItem(item));
  1527. });
  1528. }
  1529. else if (util.isNumber(ids) || util.isString(ids)) {
  1530. var item = me._castItem(me.data[ids], fieldTypes, fields);
  1531. this._appendRow(data, columns, item);
  1532. }
  1533. else if (ids instanceof Array) {
  1534. ids.forEach(function (id) {
  1535. var item = me._castItem(me.data[id], fieldTypes, fields);
  1536. me._appendRow(data, columns, item);
  1537. });
  1538. }
  1539. else {
  1540. throw new TypeError('Parameter "ids" must be ' +
  1541. 'undefined, a String, Number, or Array');
  1542. }
  1543. }
  1544. else {
  1545. // return an array
  1546. data = data || [];
  1547. if (ids == undefined) {
  1548. // return all data
  1549. util.forEach(this.data, function (item) {
  1550. data.push(me._castItem(item, fieldTypes, fields));
  1551. });
  1552. }
  1553. else if (util.isNumber(ids) || util.isString(ids)) {
  1554. // return a single item
  1555. return this._castItem(me.data[ids], fieldTypes, fields);
  1556. }
  1557. else if (ids instanceof Array) {
  1558. ids.forEach(function (id) {
  1559. data.push(me._castItem(me.data[id], fieldTypes, fields));
  1560. });
  1561. }
  1562. else {
  1563. throw new TypeError('Parameter "ids" must be ' +
  1564. 'undefined, a String, Number, or Array');
  1565. }
  1566. }
  1567. return data;
  1568. };
  1569. /**
  1570. * Remove an object by pointer or by id
  1571. * @param {String | Number | Object | Array} id Object or id, or an array with
  1572. * objects or ids to be removed
  1573. * @param {String} [senderId] Optional sender id, used to trigger events for
  1574. * all but this sender's event subscribers.
  1575. */
  1576. DataSet.prototype.remove = function (id, senderId) {
  1577. var items = [],
  1578. me = this;
  1579. if (util.isNumber(id) || util.isString(id)) {
  1580. delete this.data[id];
  1581. delete this.internalIds[id];
  1582. items.push(id);
  1583. }
  1584. else if (id instanceof Array) {
  1585. id.forEach(function (id) {
  1586. me.remove(id);
  1587. });
  1588. items = items.concat(id);
  1589. }
  1590. else if (id instanceof Object) {
  1591. // search for the object
  1592. for (var i in this.data) {
  1593. if (this.data.hasOwnProperty(i)) {
  1594. if (this.data[i] == id) {
  1595. delete this.data[i];
  1596. delete this.internalIds[i];
  1597. items.push(i);
  1598. }
  1599. }
  1600. }
  1601. }
  1602. this._trigger('remove', {items: items}, senderId);
  1603. };
  1604. /**
  1605. * Clear the data
  1606. * @param {String} [senderId] Optional sender id, used to trigger events for
  1607. * all but this sender's event subscribers.
  1608. */
  1609. DataSet.prototype.clear = function (senderId) {
  1610. var ids = Object.keys(this.data);
  1611. this.data = {};
  1612. this.internalIds = {};
  1613. this._trigger('remove', {items: ids}, senderId);
  1614. };
  1615. /**
  1616. * Find the item with maximum value of a specified field
  1617. * @param {String} field
  1618. * @return {Object} item Item containing max value, or null if no items
  1619. */
  1620. DataSet.prototype.max = function (field) {
  1621. var data = this.data,
  1622. ids = Object.keys(data);
  1623. var max = null;
  1624. var maxField = null;
  1625. ids.forEach(function (id) {
  1626. var item = data[id];
  1627. var itemField = item[field];
  1628. if (itemField != null && (!max || itemField > maxField)) {
  1629. max = item;
  1630. maxField = itemField;
  1631. }
  1632. });
  1633. return max;
  1634. };
  1635. /**
  1636. * Find the item with minimum value of a specified field
  1637. * @param {String} field
  1638. */
  1639. DataSet.prototype.min = function (field) {
  1640. var data = this.data,
  1641. ids = Object.keys(data);
  1642. var min = null;
  1643. var minField = null;
  1644. ids.forEach(function (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. return min;
  1653. };
  1654. /**
  1655. * Add a single item
  1656. * @param {Object} item
  1657. * @return {String} id
  1658. * @private
  1659. */
  1660. DataSet.prototype._addItem = function (item) {
  1661. var id = item[this.fieldId];
  1662. if (id == undefined) {
  1663. // generate an id
  1664. id = util.randomUUID();
  1665. item[this.fieldId] = id;
  1666. this.internalIds[id] = item;
  1667. }
  1668. var d = {};
  1669. for (var field in item) {
  1670. if (item.hasOwnProperty(field)) {
  1671. var type = this.fieldTypes[field]; // type may be undefined
  1672. d[field] = util.cast(item[field], type);
  1673. }
  1674. }
  1675. this.data[id] = d;
  1676. //TODO: fail when an item with this id already exists?
  1677. return id;
  1678. };
  1679. /**
  1680. * Cast and filter the fields of an item
  1681. * @param {Object | undefined} item
  1682. * @param {Object.<String, String>} [fieldTypes]
  1683. * @param {String[]} [fields]
  1684. * @return {Object | null} castedItem
  1685. * @private
  1686. */
  1687. DataSet.prototype._castItem = function (item, fieldTypes, fields) {
  1688. var clone,
  1689. fieldId = this.fieldId,
  1690. internalIds = this.internalIds;
  1691. if (item) {
  1692. clone = {};
  1693. fieldTypes = fieldTypes || {};
  1694. if (fields) {
  1695. // output filtered fields
  1696. util.forEach(item, function (value, field) {
  1697. if (fields.indexOf(field) != -1) {
  1698. clone[field] = util.cast(value, fieldTypes[field]);
  1699. }
  1700. });
  1701. }
  1702. else {
  1703. // output all fields, except internal ids
  1704. util.forEach(item, function (value, field) {
  1705. if (field != fieldId || !(value in internalIds)) {
  1706. clone[field] = util.cast(value, fieldTypes[field]);
  1707. }
  1708. });
  1709. }
  1710. }
  1711. else {
  1712. clone = null;
  1713. }
  1714. return clone;
  1715. };
  1716. /**
  1717. * Update a single item: merge with existing item
  1718. * @param {Object} item
  1719. * @return {String} id
  1720. * @private
  1721. */
  1722. DataSet.prototype._updateItem = function (item) {
  1723. var id = item[this.fieldId];
  1724. if (id == undefined) {
  1725. throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
  1726. }
  1727. var d = this.data[id];
  1728. if (d) {
  1729. // merge with current item
  1730. for (var field in item) {
  1731. if (item.hasOwnProperty(field)) {
  1732. var type = this.fieldTypes[field]; // type may be undefined
  1733. d[field] = util.cast(item[field], type);
  1734. }
  1735. }
  1736. }
  1737. else {
  1738. // create new item
  1739. this._addItem(item);
  1740. }
  1741. return id;
  1742. };
  1743. /**
  1744. * Get an array with the column names of a Google DataTable
  1745. * @param {DataTable} dataTable
  1746. * @return {Array} columnNames
  1747. * @private
  1748. */
  1749. DataSet.prototype._getColumnNames = function (dataTable) {
  1750. var columns = [];
  1751. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1752. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1753. }
  1754. return columns;
  1755. };
  1756. /**
  1757. * Append an item as a row to the dataTable
  1758. * @param dataTable
  1759. * @param columns
  1760. * @param item
  1761. * @private
  1762. */
  1763. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1764. var row = dataTable.addRow();
  1765. columns.forEach(function (field, col) {
  1766. dataTable.setValue(row, col, item[field]);
  1767. });
  1768. };
  1769. // exports
  1770. vis.DataSet = DataSet;
  1771. /**
  1772. * @constructor Stack
  1773. * Stacks items on top of each other.
  1774. * @param {ItemSet} parent
  1775. * @param {Object} [options]
  1776. */
  1777. function Stack (parent, options) {
  1778. this.parent = parent;
  1779. this.options = {
  1780. order: function (a, b) {
  1781. return (b.width - a.width) || (a.left - b.left);
  1782. }
  1783. };
  1784. this.ordered = []; // ordered items
  1785. this.setOptions(options);
  1786. }
  1787. /**
  1788. * Set options for the stack
  1789. * @param {Object} options Available options:
  1790. * {ItemSet} parent
  1791. * {Number} margin
  1792. * {function} order Stacking order
  1793. */
  1794. Stack.prototype.setOptions = function (options) {
  1795. util.extend(this.options, options);
  1796. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  1797. };
  1798. /**
  1799. * Stack the items such that they don't overlap. The items will have a minimal
  1800. * distance equal to options.margin.item.
  1801. */
  1802. Stack.prototype.update = function() {
  1803. this._order();
  1804. this._stack();
  1805. };
  1806. /**
  1807. * Order the items. The items are ordered by width first, and by left position
  1808. * second.
  1809. * If a custom order function has been provided via the options, then this will
  1810. * be used.
  1811. * @private
  1812. */
  1813. Stack.prototype._order = function() {
  1814. var items = this.parent.items;
  1815. if (!items) {
  1816. throw new Error('Cannot stack items: parent does not contain items');
  1817. }
  1818. // TODO: store the sorted items, to have less work later on
  1819. var ordered = [];
  1820. var index = 0;
  1821. util.forEach(items, function (item, id) {
  1822. ordered[index] = item;
  1823. index++;
  1824. });
  1825. //if a customer stack order function exists, use it.
  1826. var order = this.options.order;
  1827. if (!(typeof this.options.order === 'function')) {
  1828. throw new Error('Option order must be a function');
  1829. }
  1830. ordered.sort(order);
  1831. this.ordered = ordered;
  1832. };
  1833. /**
  1834. * Adjust vertical positions of the events such that they don't overlap each
  1835. * other.
  1836. * @private
  1837. */
  1838. Stack.prototype._stack = function() {
  1839. var i,
  1840. iMax,
  1841. ordered = this.ordered,
  1842. options = this.options,
  1843. axisOnTop = (options.orientation == 'top'),
  1844. margin = options.margin && options.margin.item || 0;
  1845. // calculate new, non-overlapping positions
  1846. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  1847. var item = ordered[i];
  1848. var collidingItem = null;
  1849. do {
  1850. // TODO: optimize checking for overlap. when there is a gap without items,
  1851. // you only need to check for items from the next item on, not from zero
  1852. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  1853. if (collidingItem != null) {
  1854. // There is a collision. Reposition the event above the colliding element
  1855. if (axisOnTop) {
  1856. item.top = collidingItem.top + collidingItem.height + margin;
  1857. }
  1858. else {
  1859. item.top = collidingItem.top - item.height - margin;
  1860. }
  1861. }
  1862. } while (collidingItem);
  1863. }
  1864. };
  1865. /**
  1866. * Check if the destiny position of given item overlaps with any
  1867. * of the other items from index itemStart to itemEnd.
  1868. * @param {Array} items Array with items
  1869. * @param {int} itemIndex Number of the item to be checked for overlap
  1870. * @param {int} itemStart First item to be checked.
  1871. * @param {int} itemEnd Last item to be checked.
  1872. * @return {Object | null} colliding item, or undefined when no collisions
  1873. * @param {Number} margin A minimum required margin.
  1874. * If margin is provided, the two items will be
  1875. * marked colliding when they overlap or
  1876. * when the margin between the two is smaller than
  1877. * the requested margin.
  1878. */
  1879. Stack.prototype.checkOverlap = function(items, itemIndex, itemStart, itemEnd, margin) {
  1880. var collision = this.collision;
  1881. // we loop from end to start, as we suppose that the chance of a
  1882. // collision is larger for items at the end, so check these first.
  1883. var a = items[itemIndex];
  1884. for (var i = itemEnd; i >= itemStart; i--) {
  1885. var b = items[i];
  1886. if (collision(a, b, margin)) {
  1887. if (i != itemIndex) {
  1888. return b;
  1889. }
  1890. }
  1891. }
  1892. return null;
  1893. };
  1894. /**
  1895. * Test if the two provided items collide
  1896. * The items must have parameters left, width, top, and height.
  1897. * @param {Component} a The first item
  1898. * @param {Component} b The second item
  1899. * @param {Number} margin A minimum required margin.
  1900. * If margin is provided, the two items will be
  1901. * marked colliding when they overlap or
  1902. * when the margin between the two is smaller than
  1903. * the requested margin.
  1904. * @return {boolean} true if a and b collide, else false
  1905. */
  1906. Stack.prototype.collision = function(a, b, margin) {
  1907. return ((a.left - margin) < (b.left + b.width) &&
  1908. (a.left + a.width + margin) > b.left &&
  1909. (a.top - margin) < (b.top + b.height) &&
  1910. (a.top + a.height + margin) > b.top);
  1911. };
  1912. // exports
  1913. vis.Stack = Stack;
  1914. /**
  1915. * @constructor Range
  1916. * A Range controls a numeric range with a start and end value.
  1917. * The Range adjusts the range based on mouse events or programmatic changes,
  1918. * and triggers events when the range is changing or has been changed.
  1919. * @param {Object} [options] See description at Range.setOptions
  1920. * @extends Controller
  1921. */
  1922. function Range(options) {
  1923. this.id = util.randomUUID();
  1924. this.start = 0; // Number
  1925. this.end = 0; // Number
  1926. this.options = {
  1927. min: null,
  1928. max: null,
  1929. zoomMin: null,
  1930. zoomMax: null
  1931. };
  1932. this.setOptions(options);
  1933. this.listeners = [];
  1934. }
  1935. /**
  1936. * Set options for the range controller
  1937. * @param {Object} options Available options:
  1938. * {Number} start Set start value of the range
  1939. * {Number} end Set end value of the range
  1940. * {Number} min Minimum value for start
  1941. * {Number} max Maximum value for end
  1942. * {Number} zoomMin Set a minimum value for
  1943. * (end - start).
  1944. * {Number} zoomMax Set a maximum value for
  1945. * (end - start).
  1946. */
  1947. Range.prototype.setOptions = function (options) {
  1948. util.extend(this.options, options);
  1949. if (options.start != null || options.end != null) {
  1950. this.setRange(options.start, options.end);
  1951. }
  1952. };
  1953. /**
  1954. * Add listeners for mouse and touch events to the component
  1955. * @param {Component} component
  1956. * @param {String} event Available events: 'move', 'zoom'
  1957. * @param {String} direction Available directions: 'horizontal', 'vertical'
  1958. */
  1959. Range.prototype.subscribe = function (component, event, direction) {
  1960. var me = this;
  1961. var listener;
  1962. if (direction != 'horizontal' && direction != 'vertical') {
  1963. throw new TypeError('Unknown direction "' + direction + '". ' +
  1964. 'Choose "horizontal" or "vertical".');
  1965. }
  1966. //noinspection FallthroughInSwitchStatementJS
  1967. if (event == 'move') {
  1968. listener = {
  1969. component: component,
  1970. event: event,
  1971. direction: direction,
  1972. callback: function (event) {
  1973. me._onMouseDown(event, listener);
  1974. },
  1975. params: {}
  1976. };
  1977. component.on('mousedown', listener.callback);
  1978. me.listeners.push(listener);
  1979. }
  1980. else if (event == 'zoom') {
  1981. listener = {
  1982. component: component,
  1983. event: event,
  1984. direction: direction,
  1985. callback: function (event) {
  1986. me._onMouseWheel(event, listener);
  1987. },
  1988. params: {}
  1989. };
  1990. component.on('mousewheel', listener.callback);
  1991. me.listeners.push(listener);
  1992. }
  1993. else {
  1994. throw new TypeError('Unknown event "' + event + '". ' +
  1995. 'Choose "move" or "zoom".');
  1996. }
  1997. };
  1998. /**
  1999. * Event handler
  2000. * @param {String} event name of the event, for example 'click', 'mousemove'
  2001. * @param {function} callback callback handler, invoked with the raw HTML Event
  2002. * as parameter.
  2003. */
  2004. Range.prototype.on = function (event, callback) {
  2005. events.addListener(this, event, callback);
  2006. };
  2007. /**
  2008. * Trigger an event
  2009. * @param {String} event name of the event, available events: 'rangechange',
  2010. * 'rangechanged'
  2011. * @private
  2012. */
  2013. Range.prototype._trigger = function (event) {
  2014. events.trigger(this, event, {
  2015. start: this.start,
  2016. end: this.end
  2017. });
  2018. };
  2019. /**
  2020. * Set a new start and end range
  2021. * @param {Number} start
  2022. * @param {Number} end
  2023. */
  2024. Range.prototype.setRange = function(start, end) {
  2025. var changed = this._applyRange(start, end);
  2026. if (changed) {
  2027. this._trigger('rangechange');
  2028. this._trigger('rangechanged');
  2029. }
  2030. };
  2031. /**
  2032. * Set a new start and end range. This method is the same as setRange, but
  2033. * does not trigger a range change and range changed event, and it returns
  2034. * true when the range is changed
  2035. * @param {Number} start
  2036. * @param {Number} end
  2037. * @return {Boolean} changed
  2038. * @private
  2039. */
  2040. Range.prototype._applyRange = function(start, end) {
  2041. var newStart = (start != null) ? util.cast(start, 'Number') : this.start;
  2042. var newEnd = (end != null) ? util.cast(end, 'Number') : this.end;
  2043. var diff;
  2044. // check for valid number
  2045. if (isNaN(newStart)) {
  2046. throw new Error('Invalid start "' + start + '"');
  2047. }
  2048. if (isNaN(newEnd)) {
  2049. throw new Error('Invalid end "' + end + '"');
  2050. }
  2051. // prevent start < end
  2052. if (newEnd < newStart) {
  2053. newEnd = newStart;
  2054. }
  2055. // prevent start < min
  2056. if (this.options.min != null) {
  2057. var min = this.options.min.valueOf();
  2058. if (newStart < min) {
  2059. diff = (min - newStart);
  2060. newStart += diff;
  2061. newEnd += diff;
  2062. }
  2063. }
  2064. // prevent end > max
  2065. if (this.options.max != null) {
  2066. var max = this.options.max.valueOf();
  2067. if (newEnd > max) {
  2068. diff = (newEnd - max);
  2069. newStart -= diff;
  2070. newEnd -= diff;
  2071. }
  2072. }
  2073. // prevent (end-start) > zoomMin
  2074. if (this.options.zoomMin != null) {
  2075. var zoomMin = this.options.zoomMin.valueOf();
  2076. if (zoomMin < 0) {
  2077. zoomMin = 0;
  2078. }
  2079. if ((newEnd - newStart) < zoomMin) {
  2080. if ((this.end - this.start) > zoomMin) {
  2081. // zoom to the minimum
  2082. diff = (zoomMin - (newEnd - newStart));
  2083. newStart -= diff / 2;
  2084. newEnd += diff / 2;
  2085. }
  2086. else {
  2087. // ingore this action, we are already zoomed to the minimum
  2088. newStart = this.start;
  2089. newEnd = this.end;
  2090. }
  2091. }
  2092. }
  2093. // prevent (end-start) > zoomMin
  2094. if (this.options.zoomMax != null) {
  2095. var zoomMax = this.options.zoomMax.valueOf();
  2096. if (zoomMax < 0) {
  2097. zoomMax = 0;
  2098. }
  2099. if ((newEnd - newStart) > zoomMax) {
  2100. if ((this.end - this.start) < zoomMax) {
  2101. // zoom to the maximum
  2102. diff = ((newEnd - newStart) - zoomMax);
  2103. newStart += diff / 2;
  2104. newEnd -= diff / 2;
  2105. }
  2106. else {
  2107. // ingore this action, we are already zoomed to the maximum
  2108. newStart = this.start;
  2109. newEnd = this.end;
  2110. }
  2111. }
  2112. }
  2113. var changed = (this.start != newStart || this.end != newEnd);
  2114. this.start = newStart;
  2115. this.end = newEnd;
  2116. return changed;
  2117. };
  2118. /**
  2119. * Retrieve the current range.
  2120. * @return {Object} An object with start and end properties
  2121. */
  2122. Range.prototype.getRange = function() {
  2123. return {
  2124. start: this.start,
  2125. end: this.end
  2126. };
  2127. };
  2128. /**
  2129. * Calculate the conversion offset and factor for current range, based on
  2130. * the provided width
  2131. * @param {Number} width
  2132. * @returns {{offset: number, factor: number}} conversion
  2133. */
  2134. Range.prototype.conversion = function (width) {
  2135. var start = this.start;
  2136. var end = this.end;
  2137. return Range.conversion(this.start, this.end, width);
  2138. };
  2139. /**
  2140. * Static method to calculate the conversion offset and factor for a range,
  2141. * based on the provided start, end, and width
  2142. * @param {Number} start
  2143. * @param {Number} end
  2144. * @param {Number} width
  2145. * @returns {{offset: number, factor: number}} conversion
  2146. */
  2147. Range.conversion = function (start, end, width) {
  2148. if (width != 0 && (end - start != 0)) {
  2149. return {
  2150. offset: start,
  2151. factor: width / (end - start)
  2152. }
  2153. }
  2154. else {
  2155. return {
  2156. offset: 0,
  2157. factor: 1
  2158. };
  2159. }
  2160. };
  2161. /**
  2162. * Start moving horizontally or vertically
  2163. * @param {Event} event
  2164. * @param {Object} listener Listener containing the component and params
  2165. * @private
  2166. */
  2167. Range.prototype._onMouseDown = function(event, listener) {
  2168. event = event || window.event;
  2169. var params = listener.params;
  2170. // only react on left mouse button down
  2171. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2172. if (!leftButtonDown) {
  2173. return;
  2174. }
  2175. // get mouse position
  2176. params.mouseX = util.getPageX(event);
  2177. params.mouseY = util.getPageY(event);
  2178. params.previousLeft = 0;
  2179. params.previousOffset = 0;
  2180. params.moved = false;
  2181. params.start = this.start;
  2182. params.end = this.end;
  2183. var frame = listener.component.frame;
  2184. if (frame) {
  2185. frame.style.cursor = 'move';
  2186. }
  2187. // add event listeners to handle moving the contents
  2188. // we store the function onmousemove and onmouseup in the timeaxis,
  2189. // so we can remove the eventlisteners lateron in the function onmouseup
  2190. var me = this;
  2191. if (!params.onMouseMove) {
  2192. params.onMouseMove = function (event) {
  2193. me._onMouseMove(event, listener);
  2194. };
  2195. util.addEventListener(document, "mousemove", params.onMouseMove);
  2196. }
  2197. if (!params.onMouseUp) {
  2198. params.onMouseUp = function (event) {
  2199. me._onMouseUp(event, listener);
  2200. };
  2201. util.addEventListener(document, "mouseup", params.onMouseUp);
  2202. }
  2203. util.preventDefault(event);
  2204. };
  2205. /**
  2206. * Perform moving operating.
  2207. * This function activated from within the funcion TimeAxis._onMouseDown().
  2208. * @param {Event} event
  2209. * @param {Object} listener
  2210. * @private
  2211. */
  2212. Range.prototype._onMouseMove = function (event, listener) {
  2213. event = event || window.event;
  2214. var params = listener.params;
  2215. // calculate change in mouse position
  2216. var mouseX = util.getPageX(event);
  2217. var mouseY = util.getPageY(event);
  2218. if (params.mouseX == undefined) {
  2219. params.mouseX = mouseX;
  2220. }
  2221. if (params.mouseY == undefined) {
  2222. params.mouseY = mouseY;
  2223. }
  2224. var diffX = mouseX - params.mouseX;
  2225. var diffY = mouseY - params.mouseY;
  2226. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  2227. // if mouse movement is big enough, register it as a "moved" event
  2228. if (Math.abs(diff) >= 1) {
  2229. params.moved = true;
  2230. }
  2231. var interval = (params.end - params.start);
  2232. var width = (listener.direction == 'horizontal') ?
  2233. listener.component.width : listener.component.height;
  2234. var diffRange = -diff / width * interval;
  2235. this._applyRange(params.start + diffRange, params.end + diffRange);
  2236. // fire a rangechange event
  2237. this._trigger('rangechange');
  2238. util.preventDefault(event);
  2239. };
  2240. /**
  2241. * Stop moving operating.
  2242. * This function activated from within the function Range._onMouseDown().
  2243. * @param {event} event
  2244. * @param {Object} listener
  2245. * @private
  2246. */
  2247. Range.prototype._onMouseUp = function (event, listener) {
  2248. event = event || window.event;
  2249. var params = listener.params;
  2250. if (listener.component.frame) {
  2251. listener.component.frame.style.cursor = 'auto';
  2252. }
  2253. // remove event listeners here, important for Safari
  2254. if (params.onMouseMove) {
  2255. util.removeEventListener(document, "mousemove", params.onMouseMove);
  2256. params.onMouseMove = null;
  2257. }
  2258. if (params.onMouseUp) {
  2259. util.removeEventListener(document, "mouseup", params.onMouseUp);
  2260. params.onMouseUp = null;
  2261. }
  2262. //util.preventDefault(event);
  2263. if (params.moved) {
  2264. // fire a rangechanged event
  2265. this._trigger('rangechanged');
  2266. }
  2267. };
  2268. /**
  2269. * Event handler for mouse wheel event, used to zoom
  2270. * Code from http://adomas.org/javascript-mouse-wheel/
  2271. * @param {Event} event
  2272. * @param {Object} listener
  2273. * @private
  2274. */
  2275. Range.prototype._onMouseWheel = function(event, listener) {
  2276. event = event || window.event;
  2277. // retrieve delta
  2278. var delta = 0;
  2279. if (event.wheelDelta) { /* IE/Opera. */
  2280. delta = event.wheelDelta / 120;
  2281. } else if (event.detail) { /* Mozilla case. */
  2282. // In Mozilla, sign of delta is different than in IE.
  2283. // Also, delta is multiple of 3.
  2284. delta = -event.detail / 3;
  2285. }
  2286. // If delta is nonzero, handle it.
  2287. // Basically, delta is now positive if wheel was scrolled up,
  2288. // and negative, if wheel was scrolled down.
  2289. if (delta) {
  2290. var me = this;
  2291. var zoom = function () {
  2292. // perform the zoom action. Delta is normally 1 or -1
  2293. var zoomFactor = delta / 5.0;
  2294. var zoomAround = null;
  2295. var frame = listener.component.frame;
  2296. if (frame) {
  2297. var size, conversion;
  2298. if (listener.direction == 'horizontal') {
  2299. size = listener.component.width;
  2300. conversion = me.conversion(size);
  2301. var frameLeft = util.getAbsoluteLeft(frame);
  2302. var mouseX = util.getPageX(event);
  2303. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  2304. }
  2305. else {
  2306. size = listener.component.height;
  2307. conversion = me.conversion(size);
  2308. var frameTop = util.getAbsoluteTop(frame);
  2309. var mouseY = util.getPageY(event);
  2310. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  2311. }
  2312. }
  2313. me.zoom(zoomFactor, zoomAround);
  2314. };
  2315. zoom();
  2316. }
  2317. // Prevent default actions caused by mouse wheel.
  2318. // That might be ugly, but we handle scrolls somehow
  2319. // anyway, so don't bother here...
  2320. util.preventDefault(event);
  2321. };
  2322. /**
  2323. * Zoom the range the given zoomfactor in or out. Start and end date will
  2324. * be adjusted, and the timeline will be redrawn. You can optionally give a
  2325. * date around which to zoom.
  2326. * For example, try zoomfactor = 0.1 or -0.1
  2327. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  2328. * negative value will zoom out
  2329. * @param {Number} zoomAround Value around which will be zoomed. Optional
  2330. */
  2331. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  2332. // if zoomAroundDate is not provided, take it half between start Date and end Date
  2333. if (zoomAround == null) {
  2334. zoomAround = (this.start + this.end) / 2;
  2335. }
  2336. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  2337. // result in a start>=end )
  2338. if (zoomFactor >= 1) {
  2339. zoomFactor = 0.9;
  2340. }
  2341. if (zoomFactor <= -1) {
  2342. zoomFactor = -0.9;
  2343. }
  2344. // adjust a negative factor such that zooming in with 0.1 equals zooming
  2345. // out with a factor -0.1
  2346. if (zoomFactor < 0) {
  2347. zoomFactor = zoomFactor / (1 + zoomFactor);
  2348. }
  2349. // zoom start and end relative to the zoomAround value
  2350. var startDiff = (this.start - zoomAround);
  2351. var endDiff = (this.end - zoomAround);
  2352. // calculate new start and end
  2353. var newStart = this.start - startDiff * zoomFactor;
  2354. var newEnd = this.end - endDiff * zoomFactor;
  2355. this.setRange(newStart, newEnd);
  2356. };
  2357. /**
  2358. * Move the range with a given factor to the left or right. Start and end
  2359. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  2360. * @param {Number} moveFactor Moving amount. Positive value will move right,
  2361. * negative value will move left
  2362. */
  2363. Range.prototype.move = function(moveFactor) {
  2364. // zoom start Date and end Date relative to the zoomAroundDate
  2365. var diff = (this.end - this.start);
  2366. // apply new values
  2367. var newStart = this.start + diff * moveFactor;
  2368. var newEnd = this.end + diff * moveFactor;
  2369. // TODO: reckon with min and max range
  2370. this.start = newStart;
  2371. this.end = newEnd;
  2372. };
  2373. // exports
  2374. vis.Range = Range;
  2375. /**
  2376. * @constructor Controller
  2377. *
  2378. * A Controller controls the reflows and repaints of all visual components
  2379. */
  2380. function Controller () {
  2381. this.id = util.randomUUID();
  2382. this.components = {};
  2383. this.repaintTimer = undefined;
  2384. this.reflowTimer = undefined;
  2385. }
  2386. /**
  2387. * Add a component to the controller
  2388. * @param {Component | Controller} component
  2389. */
  2390. Controller.prototype.add = function (component) {
  2391. // validate the component
  2392. if (component.id == undefined) {
  2393. throw new Error('Component has no field id');
  2394. }
  2395. if (!(component instanceof Component) && !(component instanceof Controller)) {
  2396. throw new TypeError('Component must be an instance of ' +
  2397. 'prototype Component or Controller');
  2398. }
  2399. // add the component
  2400. component.controller = this;
  2401. this.components[component.id] = component;
  2402. };
  2403. /**
  2404. * Request a reflow. The controller will schedule a reflow
  2405. */
  2406. Controller.prototype.requestReflow = function () {
  2407. if (!this.reflowTimer) {
  2408. var me = this;
  2409. this.reflowTimer = setTimeout(function () {
  2410. me.reflowTimer = undefined;
  2411. me.reflow();
  2412. }, 0);
  2413. }
  2414. };
  2415. /**
  2416. * Request a repaint. The controller will schedule a repaint
  2417. */
  2418. Controller.prototype.requestRepaint = function () {
  2419. if (!this.repaintTimer) {
  2420. var me = this;
  2421. this.repaintTimer = setTimeout(function () {
  2422. me.repaintTimer = undefined;
  2423. me.repaint();
  2424. }, 0);
  2425. }
  2426. };
  2427. /**
  2428. * Repaint all components
  2429. */
  2430. Controller.prototype.repaint = function () {
  2431. var changed = false;
  2432. // cancel any running repaint request
  2433. if (this.repaintTimer) {
  2434. clearTimeout(this.repaintTimer);
  2435. this.repaintTimer = undefined;
  2436. }
  2437. var done = {};
  2438. function repaint(component, id) {
  2439. if (!(id in done)) {
  2440. // first repaint the components on which this component is dependent
  2441. if (component.depends) {
  2442. component.depends.forEach(function (dep) {
  2443. repaint(dep, dep.id);
  2444. });
  2445. }
  2446. if (component.parent) {
  2447. repaint(component.parent, component.parent.id);
  2448. }
  2449. // repaint the component itself and mark as done
  2450. changed = component.repaint() || changed;
  2451. done[id] = true;
  2452. }
  2453. }
  2454. util.forEach(this.components, repaint);
  2455. // immediately reflow when needed
  2456. if (changed) {
  2457. this.reflow();
  2458. }
  2459. // TODO: limit the number of nested reflows/repaints, prevent loop
  2460. };
  2461. /**
  2462. * Reflow all components
  2463. */
  2464. Controller.prototype.reflow = function () {
  2465. var resized = false;
  2466. // cancel any running repaint request
  2467. if (this.reflowTimer) {
  2468. clearTimeout(this.reflowTimer);
  2469. this.reflowTimer = undefined;
  2470. }
  2471. var done = {};
  2472. function reflow(component, id) {
  2473. if (!(id in done)) {
  2474. // first reflow the components on which this component is dependent
  2475. if (component.depends) {
  2476. component.depends.forEach(function (dep) {
  2477. reflow(dep, dep.id);
  2478. });
  2479. }
  2480. if (component.parent) {
  2481. reflow(component.parent, component.parent.id);
  2482. }
  2483. // reflow the component itself and mark as done
  2484. resized = component.reflow() || resized;
  2485. done[id] = true;
  2486. }
  2487. }
  2488. util.forEach(this.components, reflow);
  2489. // immediately repaint when needed
  2490. if (resized) {
  2491. this.repaint();
  2492. }
  2493. // TODO: limit the number of nested reflows/repaints, prevent loop
  2494. };
  2495. // exports
  2496. vis.Controller = Controller;
  2497. /**
  2498. * Prototype for visual components
  2499. */
  2500. function Component () {
  2501. this.id = null;
  2502. this.parent = null;
  2503. this.depends = null;
  2504. this.controller = null;
  2505. this.options = null;
  2506. this.frame = null; // main DOM element
  2507. this.top = 0;
  2508. this.left = 0;
  2509. this.width = 0;
  2510. this.height = 0;
  2511. }
  2512. /**
  2513. * Set parameters for the frame. Parameters will be merged in current parameter
  2514. * set.
  2515. * @param {Object} options Available parameters:
  2516. * {String | function} [className]
  2517. * {String | Number | function} [left]
  2518. * {String | Number | function} [top]
  2519. * {String | Number | function} [width]
  2520. * {String | Number | function} [height]
  2521. */
  2522. Component.prototype.setOptions = function(options) {
  2523. if (options) {
  2524. util.extend(this.options, options);
  2525. }
  2526. if (this.controller) {
  2527. this.requestRepaint();
  2528. this.requestReflow();
  2529. }
  2530. };
  2531. /**
  2532. * Get the container element of the component, which can be used by a child to
  2533. * add its own widgets. Not all components do have a container for childs, in
  2534. * that case null is returned.
  2535. * @returns {HTMLElement | null} container
  2536. */
  2537. Component.prototype.getContainer = function () {
  2538. // should be implemented by the component
  2539. return null;
  2540. };
  2541. /**
  2542. * Get the frame element of the component, the outer HTML DOM element.
  2543. * @returns {HTMLElement | null} frame
  2544. */
  2545. Component.prototype.getFrame = function () {
  2546. return this.frame;
  2547. };
  2548. /**
  2549. * Repaint the component
  2550. * @return {Boolean} changed
  2551. */
  2552. Component.prototype.repaint = function () {
  2553. // should be implemented by the component
  2554. return false;
  2555. };
  2556. /**
  2557. * Reflow the component
  2558. * @return {Boolean} resized
  2559. */
  2560. Component.prototype.reflow = function () {
  2561. // should be implemented by the component
  2562. return false;
  2563. };
  2564. /**
  2565. * Request a repaint. The controller will schedule a repaint
  2566. */
  2567. Component.prototype.requestRepaint = function () {
  2568. if (this.controller) {
  2569. this.controller.requestRepaint();
  2570. }
  2571. else {
  2572. throw new Error('Cannot request a repaint: no controller configured');
  2573. // TODO: just do a repaint when no parent is configured?
  2574. }
  2575. };
  2576. /**
  2577. * Request a reflow. The controller will schedule a reflow
  2578. */
  2579. Component.prototype.requestReflow = function () {
  2580. if (this.controller) {
  2581. this.controller.requestReflow();
  2582. }
  2583. else {
  2584. throw new Error('Cannot request a reflow: no controller configured');
  2585. // TODO: just do a reflow when no parent is configured?
  2586. }
  2587. };
  2588. /**
  2589. * Event handler
  2590. * @param {String} event name of the event, for example 'click', 'mousemove'
  2591. * @param {function} callback callback handler, invoked with the raw HTML Event
  2592. * as parameter.
  2593. */
  2594. Component.prototype.on = function (event, callback) {
  2595. // TODO: rethink the way of event delegation
  2596. if (this.parent) {
  2597. this.parent.on(event, callback);
  2598. }
  2599. else {
  2600. throw new Error('Cannot attach event: no root panel found');
  2601. }
  2602. };
  2603. // exports
  2604. vis.component.Component = Component;
  2605. /**
  2606. * A panel can contain components
  2607. * @param {Component} [parent]
  2608. * @param {Component[]} [depends] Components on which this components depends
  2609. * (except for the parent)
  2610. * @param {Object} [options] Available parameters:
  2611. * {String | Number | function} [left]
  2612. * {String | Number | function} [top]
  2613. * {String | Number | function} [width]
  2614. * {String | Number | function} [height]
  2615. * {String | function} [className]
  2616. * @constructor Panel
  2617. * @extends Component
  2618. */
  2619. function Panel(parent, depends, options) {
  2620. this.id = util.randomUUID();
  2621. this.parent = parent;
  2622. this.depends = depends;
  2623. this.options = {};
  2624. this.setOptions(options);
  2625. }
  2626. Panel.prototype = new Component();
  2627. /**
  2628. * Get the container element of the panel, which can be used by a child to
  2629. * add its own widgets.
  2630. * @returns {HTMLElement} container
  2631. */
  2632. Panel.prototype.getContainer = function () {
  2633. return this.frame;
  2634. };
  2635. /**
  2636. * Repaint the component
  2637. * @return {Boolean} changed
  2638. */
  2639. Panel.prototype.repaint = function () {
  2640. var changed = 0,
  2641. update = util.updateProperty,
  2642. asSize = util.option.asSize,
  2643. options = this.options,
  2644. frame = this.frame;
  2645. if (!frame) {
  2646. frame = document.createElement('div');
  2647. frame.className = 'panel';
  2648. if (options.className) {
  2649. if (typeof options.className == 'function') {
  2650. util.addClassName(frame, String(options.className()));
  2651. }
  2652. else {
  2653. util.addClassName(frame, String(options.className));
  2654. }
  2655. }
  2656. this.frame = frame;
  2657. changed += 1;
  2658. }
  2659. if (!frame.parentNode) {
  2660. if (!this.parent) {
  2661. throw new Error('Cannot repaint panel: no parent attached');
  2662. }
  2663. var parentContainer = this.parent.getContainer();
  2664. if (!parentContainer) {
  2665. throw new Error('Cannot repaint panel: parent has no container element');
  2666. }
  2667. parentContainer.appendChild(frame);
  2668. changed += 1;
  2669. }
  2670. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  2671. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  2672. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  2673. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  2674. return (changed > 0);
  2675. };
  2676. /**
  2677. * Reflow the component
  2678. * @return {Boolean} resized
  2679. */
  2680. Panel.prototype.reflow = function () {
  2681. var changed = 0,
  2682. update = util.updateProperty,
  2683. frame = this.frame;
  2684. if (frame) {
  2685. changed += update(this, 'top', frame.offsetTop);
  2686. changed += update(this, 'left', frame.offsetLeft);
  2687. changed += update(this, 'width', frame.offsetWidth);
  2688. changed += update(this, 'height', frame.offsetHeight);
  2689. }
  2690. else {
  2691. changed += 1;
  2692. }
  2693. return (changed > 0);
  2694. };
  2695. // exports
  2696. vis.component.Panel = Panel;
  2697. /**
  2698. * A root panel can hold components. The root panel must be initialized with
  2699. * a DOM element as container.
  2700. * @param {HTMLElement} container
  2701. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  2702. * @constructor RootPanel
  2703. * @extends Panel
  2704. */
  2705. function RootPanel(container, options) {
  2706. this.id = util.randomUUID();
  2707. this.container = container;
  2708. this.options = {
  2709. autoResize: true
  2710. };
  2711. this.listeners = {}; // event listeners
  2712. this.setOptions(options);
  2713. }
  2714. RootPanel.prototype = new Panel();
  2715. /**
  2716. * Set options. Will extend the current options.
  2717. * @param {Object} [options] Available parameters:
  2718. * {String | function} [className]
  2719. * {String | Number | function} [left]
  2720. * {String | Number | function} [top]
  2721. * {String | Number | function} [width]
  2722. * {String | Number | function} [height]
  2723. * {String | Number | function} [height]
  2724. * {Boolean | function} [autoResize]
  2725. */
  2726. RootPanel.prototype.setOptions = function (options) {
  2727. util.extend(this.options, options);
  2728. if (this.options.autoResize) {
  2729. this._watch();
  2730. }
  2731. else {
  2732. this._unwatch();
  2733. }
  2734. };
  2735. /**
  2736. * Repaint the component
  2737. * @return {Boolean} changed
  2738. */
  2739. RootPanel.prototype.repaint = function () {
  2740. var changed = 0,
  2741. update = util.updateProperty,
  2742. asSize = util.option.asSize,
  2743. options = this.options,
  2744. frame = this.frame;
  2745. if (!frame) {
  2746. frame = document.createElement('div');
  2747. frame.className = 'graph panel';
  2748. if (options.className) {
  2749. util.addClassName(frame, util.option.asString(options.className));
  2750. }
  2751. this.frame = frame;
  2752. changed += 1;
  2753. }
  2754. if (!frame.parentNode) {
  2755. if (!this.container) {
  2756. throw new Error('Cannot repaint root panel: no container attached');
  2757. }
  2758. this.container.appendChild(frame);
  2759. changed += 1;
  2760. }
  2761. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  2762. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  2763. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  2764. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  2765. this._updateEventEmitters();
  2766. return (changed > 0);
  2767. };
  2768. /**
  2769. * Reflow the component
  2770. * @return {Boolean} resized
  2771. */
  2772. RootPanel.prototype.reflow = function () {
  2773. var changed = 0,
  2774. update = util.updateProperty,
  2775. frame = this.frame;
  2776. if (frame) {
  2777. changed += update(this, 'top', frame.offsetTop);
  2778. changed += update(this, 'left', frame.offsetLeft);
  2779. changed += update(this, 'width', frame.offsetWidth);
  2780. changed += update(this, 'height', frame.offsetHeight);
  2781. }
  2782. else {
  2783. changed += 1;
  2784. }
  2785. return (changed > 0);
  2786. };
  2787. /**
  2788. * Watch for changes in the size of the frame. On resize, the Panel will
  2789. * automatically redraw itself.
  2790. * @private
  2791. */
  2792. RootPanel.prototype._watch = function () {
  2793. var me = this;
  2794. this._unwatch();
  2795. var checkSize = function () {
  2796. if (!me.options.autoResize) {
  2797. // stop watching when the option autoResize is changed to false
  2798. me._unwatch();
  2799. return;
  2800. }
  2801. if (me.frame) {
  2802. // check whether the frame is resized
  2803. if ((me.frame.clientWidth != me.width) ||
  2804. (me.frame.clientHeight != me.height)) {
  2805. me.requestReflow();
  2806. }
  2807. }
  2808. };
  2809. // TODO: automatically cleanup the event listener when the frame is deleted
  2810. util.addEventListener(window, 'resize', checkSize);
  2811. this.watchTimer = setInterval(checkSize, 1000);
  2812. };
  2813. /**
  2814. * Stop watching for a resize of the frame.
  2815. * @private
  2816. */
  2817. RootPanel.prototype._unwatch = function () {
  2818. if (this.watchTimer) {
  2819. clearInterval(this.watchTimer);
  2820. this.watchTimer = undefined;
  2821. }
  2822. // TODO: remove event listener on window.resize
  2823. };
  2824. /**
  2825. * Event handler
  2826. * @param {String} event name of the event, for example 'click', 'mousemove'
  2827. * @param {function} callback callback handler, invoked with the raw HTML Event
  2828. * as parameter.
  2829. */
  2830. RootPanel.prototype.on = function (event, callback) {
  2831. // register the listener at this component
  2832. var arr = this.listeners[event];
  2833. if (!arr) {
  2834. arr = [];
  2835. this.listeners[event] = arr;
  2836. }
  2837. arr.push(callback);
  2838. this._updateEventEmitters();
  2839. };
  2840. /**
  2841. * Update the event listeners for all event emitters
  2842. * @private
  2843. */
  2844. RootPanel.prototype._updateEventEmitters = function () {
  2845. if (this.listeners) {
  2846. var me = this;
  2847. util.forEach(this.listeners, function (listeners, event) {
  2848. if (!me.emitters) {
  2849. me.emitters = {};
  2850. }
  2851. if (!(event in me.emitters)) {
  2852. // create event
  2853. var frame = me.frame;
  2854. if (frame) {
  2855. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  2856. var callback = function(event) {
  2857. listeners.forEach(function (listener) {
  2858. // TODO: filter on event target!
  2859. listener(event);
  2860. });
  2861. };
  2862. me.emitters[event] = callback;
  2863. util.addEventListener(frame, event, callback);
  2864. }
  2865. }
  2866. });
  2867. // TODO: be able to delete event listeners
  2868. // TODO: be able to move event listeners to a parent when available
  2869. }
  2870. };
  2871. // exports
  2872. vis.component.RootPanel = RootPanel;
  2873. /**
  2874. * A horizontal time axis
  2875. * @param {Component} parent
  2876. * @param {Component[]} [depends] Components on which this components depends
  2877. * (except for the parent)
  2878. * @param {Object} [options] See TimeAxis.setOptions for the available
  2879. * options.
  2880. * @constructor TimeAxis
  2881. * @extends Component
  2882. */
  2883. function TimeAxis (parent, depends, options) {
  2884. this.id = util.randomUUID();
  2885. this.parent = parent;
  2886. this.depends = depends;
  2887. this.dom = {
  2888. majorLines: [],
  2889. majorTexts: [],
  2890. minorLines: [],
  2891. minorTexts: [],
  2892. redundant: {
  2893. majorLines: [],
  2894. majorTexts: [],
  2895. minorLines: [],
  2896. minorTexts: []
  2897. }
  2898. };
  2899. this.props = {
  2900. range: {
  2901. start: 0,
  2902. end: 0,
  2903. minimumStep: 0
  2904. },
  2905. lineTop: 0
  2906. };
  2907. this.options = {
  2908. orientation: 'bottom', // supported: 'top', 'bottom'
  2909. // TODO: implement timeaxis orientations 'left' and 'right'
  2910. showMinorLabels: true,
  2911. showMajorLabels: true
  2912. };
  2913. this.conversion = null;
  2914. this.range = null;
  2915. this.setOptions(options);
  2916. }
  2917. TimeAxis.prototype = new Component();
  2918. // TODO: comment options
  2919. TimeAxis.prototype.setOptions = function (options) {
  2920. util.extend(this.options, options);
  2921. };
  2922. /**
  2923. * Set a range (start and end)
  2924. * @param {Range | Object} range A Range or an object containing start and end.
  2925. */
  2926. TimeAxis.prototype.setRange = function (range) {
  2927. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  2928. throw new TypeError('Range must be an instance of Range, ' +
  2929. 'or an object containing start and end.');
  2930. }
  2931. this.range = range;
  2932. };
  2933. /**
  2934. * Convert a position on screen (pixels) to a datetime
  2935. * @param {int} x Position on the screen in pixels
  2936. * @return {Date} time The datetime the corresponds with given position x
  2937. */
  2938. TimeAxis.prototype.toTime = function(x) {
  2939. var conversion = this.conversion;
  2940. return new Date(x / conversion.factor + conversion.offset);
  2941. };
  2942. /**
  2943. * Convert a datetime (Date object) into a position on the screen
  2944. * @param {Date} time A date
  2945. * @return {int} x The position on the screen in pixels which corresponds
  2946. * with the given date.
  2947. * @private
  2948. */
  2949. TimeAxis.prototype.toScreen = function(time) {
  2950. var conversion = this.conversion;
  2951. return (time.valueOf() - conversion.offset) * conversion.factor;
  2952. };
  2953. /**
  2954. * Repaint the component
  2955. * @return {Boolean} changed
  2956. */
  2957. TimeAxis.prototype.repaint = function () {
  2958. var changed = 0,
  2959. update = util.updateProperty,
  2960. asSize = util.option.asSize,
  2961. options = this.options,
  2962. props = this.props,
  2963. step = this.step;
  2964. var frame = this.frame;
  2965. if (!frame) {
  2966. frame = document.createElement('div');
  2967. this.frame = frame;
  2968. changed += 1;
  2969. }
  2970. frame.className = 'axis ' + options.orientation;
  2971. // TODO: custom className?
  2972. if (!frame.parentNode) {
  2973. if (!this.parent) {
  2974. throw new Error('Cannot repaint time axis: no parent attached');
  2975. }
  2976. var parentContainer = this.parent.getContainer();
  2977. if (!parentContainer) {
  2978. throw new Error('Cannot repaint time axis: parent has no container element');
  2979. }
  2980. parentContainer.appendChild(frame);
  2981. changed += 1;
  2982. }
  2983. var parent = frame.parentNode;
  2984. if (parent) {
  2985. var beforeChild = frame.nextSibling;
  2986. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  2987. var orientation = options.orientation;
  2988. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  2989. (this.props.parentHeight - this.height) + 'px' :
  2990. '0px';
  2991. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  2992. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  2993. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  2994. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  2995. // get characters width and height
  2996. this._repaintMeasureChars();
  2997. if (this.step) {
  2998. this._repaintStart();
  2999. step.first();
  3000. var xFirstMajorLabel = undefined;
  3001. var max = 0;
  3002. while (step.hasNext() && max < 1000) {
  3003. max++;
  3004. var cur = step.getCurrent(),
  3005. x = this.toScreen(cur),
  3006. isMajor = step.isMajor();
  3007. // TODO: lines must have a width, such that we can create css backgrounds
  3008. if (options.showMinorLabels) {
  3009. this._repaintMinorText(x, step.getLabelMinor());
  3010. }
  3011. if (isMajor && options.showMajorLabels) {
  3012. if (x > 0) {
  3013. if (xFirstMajorLabel == undefined) {
  3014. xFirstMajorLabel = x;
  3015. }
  3016. this._repaintMajorText(x, step.getLabelMajor());
  3017. }
  3018. this._repaintMajorLine(x);
  3019. }
  3020. else {
  3021. this._repaintMinorLine(x);
  3022. }
  3023. step.next();
  3024. }
  3025. // create a major label on the left when needed
  3026. if (options.showMajorLabels) {
  3027. var leftTime = this.toTime(0),
  3028. leftText = step.getLabelMajor(leftTime),
  3029. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3030. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3031. this._repaintMajorText(0, leftText);
  3032. }
  3033. }
  3034. this._repaintEnd();
  3035. }
  3036. this._repaintLine();
  3037. // put frame online again
  3038. if (beforeChild) {
  3039. parent.insertBefore(frame, beforeChild);
  3040. }
  3041. else {
  3042. parent.appendChild(frame)
  3043. }
  3044. }
  3045. return (changed > 0);
  3046. };
  3047. /**
  3048. * Start a repaint. Move all DOM elements to a redundant list, where they
  3049. * can be picked for re-use, or can be cleaned up in the end
  3050. * @private
  3051. */
  3052. TimeAxis.prototype._repaintStart = function () {
  3053. var dom = this.dom,
  3054. redundant = dom.redundant;
  3055. redundant.majorLines = dom.majorLines;
  3056. redundant.majorTexts = dom.majorTexts;
  3057. redundant.minorLines = dom.minorLines;
  3058. redundant.minorTexts = dom.minorTexts;
  3059. dom.majorLines = [];
  3060. dom.majorTexts = [];
  3061. dom.minorLines = [];
  3062. dom.minorTexts = [];
  3063. };
  3064. /**
  3065. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3066. * @private
  3067. */
  3068. TimeAxis.prototype._repaintEnd = function () {
  3069. util.forEach(this.dom.redundant, function (arr) {
  3070. while (arr.length) {
  3071. var elem = arr.pop();
  3072. if (elem && elem.parentNode) {
  3073. elem.parentNode.removeChild(elem);
  3074. }
  3075. }
  3076. });
  3077. };
  3078. /**
  3079. * Create a minor label for the axis at position x
  3080. * @param {Number} x
  3081. * @param {String} text
  3082. * @private
  3083. */
  3084. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3085. // reuse redundant label
  3086. var label = this.dom.redundant.minorTexts.shift();
  3087. if (!label) {
  3088. // create new label
  3089. var content = document.createTextNode('');
  3090. label = document.createElement('div');
  3091. label.appendChild(content);
  3092. label.className = 'text minor';
  3093. this.frame.appendChild(label);
  3094. }
  3095. this.dom.minorTexts.push(label);
  3096. label.childNodes[0].nodeValue = text;
  3097. label.style.left = x + 'px';
  3098. label.style.top = this.props.minorLabelTop + 'px';
  3099. //label.title = title; // TODO: this is a heavy operation
  3100. };
  3101. /**
  3102. * Create a Major label for the axis at position x
  3103. * @param {Number} x
  3104. * @param {String} text
  3105. * @private
  3106. */
  3107. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3108. // reuse redundant label
  3109. var label = this.dom.redundant.majorTexts.shift();
  3110. if (!label) {
  3111. // create label
  3112. var content = document.createTextNode(text);
  3113. label = document.createElement('div');
  3114. label.className = 'text major';
  3115. label.appendChild(content);
  3116. this.frame.appendChild(label);
  3117. }
  3118. this.dom.majorTexts.push(label);
  3119. label.childNodes[0].nodeValue = text;
  3120. label.style.top = this.props.majorLabelTop + 'px';
  3121. label.style.left = x + 'px';
  3122. //label.title = title; // TODO: this is a heavy operation
  3123. };
  3124. /**
  3125. * Create a minor line for the axis at position x
  3126. * @param {Number} x
  3127. * @private
  3128. */
  3129. TimeAxis.prototype._repaintMinorLine = function (x) {
  3130. // reuse redundant line
  3131. var line = this.dom.redundant.minorLines.shift();
  3132. if (!line) {
  3133. // create vertical line
  3134. line = document.createElement('div');
  3135. line.className = 'grid vertical minor';
  3136. this.frame.appendChild(line);
  3137. }
  3138. this.dom.minorLines.push(line);
  3139. var props = this.props;
  3140. line.style.top = props.minorLineTop + 'px';
  3141. line.style.height = props.minorLineHeight + 'px';
  3142. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3143. };
  3144. /**
  3145. * Create a Major line for the axis at position x
  3146. * @param {Number} x
  3147. * @private
  3148. */
  3149. TimeAxis.prototype._repaintMajorLine = function (x) {
  3150. // reuse redundant line
  3151. var line = this.dom.redundant.majorLines.shift();
  3152. if (!line) {
  3153. // create vertical line
  3154. line = document.createElement('DIV');
  3155. line.className = 'grid vertical major';
  3156. this.frame.appendChild(line);
  3157. }
  3158. this.dom.majorLines.push(line);
  3159. var props = this.props;
  3160. line.style.top = props.majorLineTop + 'px';
  3161. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3162. line.style.height = props.majorLineHeight + 'px';
  3163. };
  3164. /**
  3165. * Repaint the horizontal line for the axis
  3166. * @private
  3167. */
  3168. TimeAxis.prototype._repaintLine = function() {
  3169. var line = this.dom.line,
  3170. frame = this.frame,
  3171. options = this.options;
  3172. // line before all axis elements
  3173. if (options.showMinorLabels || options.showMajorLabels) {
  3174. if (line) {
  3175. // put this line at the end of all childs
  3176. frame.removeChild(line);
  3177. frame.appendChild(line);
  3178. }
  3179. else {
  3180. // create the axis line
  3181. line = document.createElement('div');
  3182. line.className = 'grid horizontal major';
  3183. frame.appendChild(line);
  3184. this.dom.line = line;
  3185. }
  3186. line.style.top = this.props.lineTop + 'px';
  3187. }
  3188. else {
  3189. if (line && axis.parentElement) {
  3190. frame.removeChild(axis.line);
  3191. delete this.dom.line;
  3192. }
  3193. }
  3194. };
  3195. /**
  3196. * Create characters used to determine the size of text on the axis
  3197. * @private
  3198. */
  3199. TimeAxis.prototype._repaintMeasureChars = function () {
  3200. // calculate the width and height of a single character
  3201. // this is used to calculate the step size, and also the positioning of the
  3202. // axis
  3203. var dom = this.dom,
  3204. text;
  3205. if (!dom.characterMinor) {
  3206. text = document.createTextNode('0');
  3207. var measureCharMinor = document.createElement('DIV');
  3208. measureCharMinor.className = 'text minor measure';
  3209. measureCharMinor.appendChild(text);
  3210. this.frame.appendChild(measureCharMinor);
  3211. dom.measureCharMinor = measureCharMinor;
  3212. }
  3213. if (!dom.characterMajor) {
  3214. text = document.createTextNode('0');
  3215. var measureCharMajor = document.createElement('DIV');
  3216. measureCharMajor.className = 'text major measure';
  3217. measureCharMajor.appendChild(text);
  3218. this.frame.appendChild(measureCharMajor);
  3219. dom.measureCharMajor = measureCharMajor;
  3220. }
  3221. };
  3222. /**
  3223. * Reflow the component
  3224. * @return {Boolean} resized
  3225. */
  3226. TimeAxis.prototype.reflow = function () {
  3227. var changed = 0,
  3228. update = util.updateProperty,
  3229. frame = this.frame,
  3230. range = this.range;
  3231. if (!range) {
  3232. throw new Error('Cannot repaint time axis: no range configured');
  3233. }
  3234. if (frame) {
  3235. changed += update(this, 'top', frame.offsetTop);
  3236. changed += update(this, 'left', frame.offsetLeft);
  3237. // calculate size of a character
  3238. var props = this.props,
  3239. showMinorLabels = this.options.showMinorLabels,
  3240. showMajorLabels = this.options.showMajorLabels,
  3241. measureCharMinor = this.dom.measureCharMinor,
  3242. measureCharMajor = this.dom.measureCharMajor;
  3243. if (measureCharMinor) {
  3244. props.minorCharHeight = measureCharMinor.clientHeight;
  3245. props.minorCharWidth = measureCharMinor.clientWidth;
  3246. }
  3247. if (measureCharMajor) {
  3248. props.majorCharHeight = measureCharMajor.clientHeight;
  3249. props.majorCharWidth = measureCharMajor.clientWidth;
  3250. }
  3251. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  3252. if (parentHeight != props.parentHeight) {
  3253. props.parentHeight = parentHeight;
  3254. changed += 1;
  3255. }
  3256. switch (this.options.orientation) {
  3257. case 'bottom':
  3258. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3259. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3260. props.minorLabelTop = 0;
  3261. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  3262. props.minorLineTop = -this.top;
  3263. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  3264. props.minorLineWidth = 1; // TODO: really calculate width
  3265. props.majorLineTop = -this.top;
  3266. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  3267. props.majorLineWidth = 1; // TODO: really calculate width
  3268. props.lineTop = 0;
  3269. break;
  3270. case 'top':
  3271. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3272. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3273. props.majorLabelTop = 0;
  3274. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  3275. props.minorLineTop = props.minorLabelTop;
  3276. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  3277. props.minorLineWidth = 1; // TODO: really calculate width
  3278. props.majorLineTop = 0;
  3279. props.majorLineHeight = Math.max(parentHeight - this.top);
  3280. props.majorLineWidth = 1; // TODO: really calculate width
  3281. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  3282. break;
  3283. default:
  3284. throw new Error('Unkown orientation "' + this.options.orientation + '"');
  3285. }
  3286. var height = props.minorLabelHeight + props.majorLabelHeight;
  3287. changed += update(this, 'width', frame.offsetWidth);
  3288. changed += update(this, 'height', height);
  3289. // calculate range and step
  3290. this._updateConversion();
  3291. var start = util.cast(range.start, 'Date'),
  3292. end = util.cast(range.end, 'Date'),
  3293. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  3294. this.step = new TimeStep(start, end, minimumStep);
  3295. changed += update(props.range, 'start', start.valueOf());
  3296. changed += update(props.range, 'end', end.valueOf());
  3297. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  3298. }
  3299. return (changed > 0);
  3300. };
  3301. /**
  3302. * Calculate the factor and offset to convert a position on screen to the
  3303. * corresponding date and vice versa.
  3304. * After the method _updateConversion is executed once, the methods toTime
  3305. * and toScreen can be used.
  3306. * @private
  3307. */
  3308. TimeAxis.prototype._updateConversion = function() {
  3309. var range = this.range;
  3310. if (!range) {
  3311. throw new Error('No range configured');
  3312. }
  3313. if (range.conversion) {
  3314. this.conversion = range.conversion(this.width);
  3315. }
  3316. else {
  3317. this.conversion = Range.conversion(range.start, range.end, this.width);
  3318. }
  3319. };
  3320. // exports
  3321. vis.component.TimeAxis = TimeAxis;
  3322. /**
  3323. * An ItemSet holds a set of items and ranges which can be displayed in a
  3324. * range. The width is determined by the parent of the ItemSet, and the height
  3325. * is determined by the size of the items.
  3326. * @param {Component} parent
  3327. * @param {Component[]} [depends] Components on which this components depends
  3328. * (except for the parent)
  3329. * @param {Object} [options] See ItemSet.setOptions for the available
  3330. * options.
  3331. * @constructor ItemSet
  3332. * @extends Panel
  3333. */
  3334. function ItemSet(parent, depends, options) {
  3335. this.id = util.randomUUID();
  3336. this.parent = parent;
  3337. this.depends = depends;
  3338. // one options object is shared by this itemset and all its items
  3339. this.options = {
  3340. style: 'box',
  3341. align: 'center',
  3342. orientation: 'bottom',
  3343. margin: {
  3344. axis: 20,
  3345. item: 10
  3346. },
  3347. padding: 5
  3348. };
  3349. var me = this;
  3350. this.data = null; // DataSet
  3351. this.range = null; // Range or Object {start: number, end: number}
  3352. this.listeners = {
  3353. 'add': function (event, params) {
  3354. me._onAdd(params.items);
  3355. },
  3356. 'update': function (event, params) {
  3357. me._onUpdate(params.items);
  3358. },
  3359. 'remove': function (event, params) {
  3360. me._onRemove(params.items);
  3361. }
  3362. };
  3363. this.items = {};
  3364. this.queue = {}; // queue with items to be added/updated/removed
  3365. this.stack = new Stack(this);
  3366. this.conversion = null;
  3367. this.setOptions(options);
  3368. }
  3369. ItemSet.prototype = new Panel();
  3370. /**
  3371. * Set options for the ItemSet. Existing options will be extended/overwritten.
  3372. * @param {Object} [options] The following options are available:
  3373. * {String | function} [className]
  3374. * class name for the itemset
  3375. * {String} [style]
  3376. * Default style for the items. Choose from 'box'
  3377. * (default), 'point', or 'range'. The default
  3378. * Style can be overwritten by individual items.
  3379. * {String} align
  3380. * Alignment for the items, only applicable for
  3381. * ItemBox. Choose 'center' (default), 'left', or
  3382. * 'right'.
  3383. * {String} orientation
  3384. * Orientation of the item set. Choose 'top' or
  3385. * 'bottom' (default).
  3386. * {Number} margin.axis
  3387. * Margin between the axis and the items in pixels.
  3388. * Default is 20.
  3389. * {Number} margin.item
  3390. * Margin between items in pixels. Default is 10.
  3391. * {Number} padding
  3392. * Padding of the contents of an item in pixels.
  3393. * Must correspond with the items css. Default is 5.
  3394. */
  3395. ItemSet.prototype.setOptions = function (options) {
  3396. util.extend(this.options, options);
  3397. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  3398. this.stack.setOptions(this.options);
  3399. };
  3400. /**
  3401. * Set range (start and end).
  3402. * @param {Range | Object} range A Range or an object containing start and end.
  3403. */
  3404. ItemSet.prototype.setRange = function (range) {
  3405. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3406. throw new TypeError('Range must be an instance of Range, ' +
  3407. 'or an object containing start and end.');
  3408. }
  3409. this.range = range;
  3410. };
  3411. /**
  3412. * Repaint the component
  3413. * @return {Boolean} changed
  3414. */
  3415. ItemSet.prototype.repaint = function () {
  3416. var changed = 0,
  3417. update = util.updateProperty,
  3418. asSize = util.option.asSize,
  3419. options = this.options,
  3420. frame = this.frame;
  3421. if (!frame) {
  3422. frame = document.createElement('div');
  3423. frame.className = 'itemset';
  3424. if (options.className) {
  3425. util.addClassName(frame, util.option.asString(options.className));
  3426. }
  3427. this.frame = frame;
  3428. changed += 1;
  3429. }
  3430. if (!frame.parentNode) {
  3431. if (!this.parent) {
  3432. throw new Error('Cannot repaint itemset: no parent attached');
  3433. }
  3434. var parentContainer = this.parent.getContainer();
  3435. if (!parentContainer) {
  3436. throw new Error('Cannot repaint itemset: parent has no container element');
  3437. }
  3438. parentContainer.appendChild(frame);
  3439. changed += 1;
  3440. }
  3441. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3442. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3443. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3444. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3445. this._updateConversion();
  3446. var me = this,
  3447. queue = this.queue,
  3448. data = this.data,
  3449. items = this.items,
  3450. dataOptions = {
  3451. fields: ['id', 'start', 'end', 'content', 'type']
  3452. };
  3453. // TODO: copy options from the itemset itself?
  3454. // TODO: make orientation dynamically changable for the items
  3455. // show/hide added/changed/removed items
  3456. Object.keys(queue).forEach(function (id) {
  3457. var entry = queue[id];
  3458. var item = entry.item;
  3459. //noinspection FallthroughInSwitchStatementJS
  3460. switch (entry.action) {
  3461. case 'add':
  3462. case 'update':
  3463. var itemData = data.get(id, dataOptions);
  3464. var type = itemData.type ||
  3465. (itemData.start && itemData.end && 'range') ||
  3466. 'box';
  3467. var constructor = vis.component.item[type];
  3468. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  3469. if (item) {
  3470. // update item
  3471. if (!constructor || !(item instanceof constructor)) {
  3472. // item type has changed, delete the item
  3473. item.visible = false;
  3474. changed += item.repaint();
  3475. item = null;
  3476. }
  3477. else {
  3478. item.data = itemData; // TODO: create a method item.setData ?
  3479. changed += item.repaint();
  3480. }
  3481. }
  3482. if (!item) {
  3483. // create item
  3484. if (constructor) {
  3485. item = new constructor(me, itemData, options);
  3486. changed += item.repaint();
  3487. }
  3488. else {
  3489. throw new TypeError('Unknown item type "' + type + '"');
  3490. }
  3491. }
  3492. // update lists
  3493. items[id] = item;
  3494. delete queue[id];
  3495. break;
  3496. case 'remove':
  3497. if (item) {
  3498. // TODO: remove dom of the item
  3499. item.visible = false;
  3500. changed += item.repaint();
  3501. }
  3502. // update lists
  3503. delete items[id];
  3504. delete queue[id];
  3505. break;
  3506. default:
  3507. console.log('Error: unknown action "' + entry.action + '"');
  3508. }
  3509. });
  3510. // reposition all items
  3511. util.forEach(this.items, function (item) {
  3512. item.reposition();
  3513. });
  3514. return (changed > 0);
  3515. };
  3516. /**
  3517. * Reflow the component
  3518. * @return {Boolean} resized
  3519. */
  3520. ItemSet.prototype.reflow = function () {
  3521. var changed = 0,
  3522. options = this.options,
  3523. update = util.updateProperty,
  3524. frame = this.frame;
  3525. if (frame) {
  3526. this._updateConversion();
  3527. util.forEach(this.items, function (item) {
  3528. changed += item.reflow();
  3529. });
  3530. // TODO: stack.update should be triggered via an event, in stack itself
  3531. // TODO: only update the stack when there are changed items
  3532. this.stack.update();
  3533. if (options.height != null) {
  3534. changed += update(this, 'height', frame.offsetHeight);
  3535. }
  3536. else {
  3537. // height is not specified, determine the height from the height and positioned items
  3538. var frameHeight = this.height;
  3539. var maxHeight = 0;
  3540. if (options.orientation == 'top') {
  3541. util.forEach(this.items, function (item) {
  3542. maxHeight = Math.max(maxHeight, item.top + item.height);
  3543. });
  3544. }
  3545. else {
  3546. // orientation == 'bottom'
  3547. util.forEach(this.items, function (item) {
  3548. maxHeight = Math.max(maxHeight, frameHeight - item.top);
  3549. });
  3550. }
  3551. changed += update(this, 'height', maxHeight + options.margin.axis);
  3552. }
  3553. // calculate height from items
  3554. changed += update(this, 'top', frame.offsetTop);
  3555. changed += update(this, 'left', frame.offsetLeft);
  3556. changed += update(this, 'width', frame.offsetWidth);
  3557. }
  3558. else {
  3559. changed += 1;
  3560. }
  3561. return (changed > 0);
  3562. };
  3563. /**
  3564. * Set data
  3565. * @param {DataSet | Array | DataTable} data
  3566. */
  3567. ItemSet.prototype.setData = function(data) {
  3568. // unsubscribe from current dataset
  3569. var current = this.data;
  3570. if (current) {
  3571. util.forEach(this.listeners, function (callback, event) {
  3572. current.unsubscribe(event, callback);
  3573. });
  3574. }
  3575. if (data instanceof DataSet) {
  3576. this.data = data;
  3577. }
  3578. else {
  3579. this.data = new DataSet({
  3580. fieldTypes: {
  3581. start: 'Date',
  3582. end: 'Date'
  3583. }
  3584. });
  3585. this.data.add(data);
  3586. }
  3587. var id = this.id;
  3588. var me = this;
  3589. util.forEach(this.listeners, function (callback, event) {
  3590. me.data.subscribe(event, callback, id);
  3591. });
  3592. var dataItems = this.data.get({filter: ['id']});
  3593. var ids = [];
  3594. util.forEach(dataItems, function (dataItem, index) {
  3595. ids[index] = dataItem.id;
  3596. });
  3597. this._onAdd(ids);
  3598. };
  3599. /**
  3600. * Get the data range of the item set.
  3601. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  3602. * When no minimum is found, min==null
  3603. * When no maximum is found, max==null
  3604. */
  3605. ItemSet.prototype.getDataRange = function () {
  3606. // calculate min from start filed
  3607. var data = this.data;
  3608. var min = data.min('start');
  3609. min = min ? min.start.valueOf() : null;
  3610. // calculate max of both start and end fields
  3611. var maxStart = data.max('start');
  3612. var maxEnd = data.max('end');
  3613. maxStart = maxStart ? maxStart.start.valueOf() : null;
  3614. maxEnd = maxEnd ? maxEnd.end.valueOf() : null;
  3615. var max = Math.max(maxStart, maxEnd);
  3616. return {
  3617. min: new Date(min),
  3618. max: new Date(max)
  3619. };
  3620. };
  3621. /**
  3622. * Handle updated items
  3623. * @param {Number[]} ids
  3624. * @private
  3625. */
  3626. ItemSet.prototype._onUpdate = function(ids) {
  3627. this._toQueue(ids, 'update');
  3628. };
  3629. /**
  3630. * Handle changed items
  3631. * @param {Number[]} ids
  3632. * @private
  3633. */
  3634. ItemSet.prototype._onAdd = function(ids) {
  3635. this._toQueue(ids, 'add');
  3636. };
  3637. /**
  3638. * Handle removed items
  3639. * @param {Number[]} ids
  3640. * @private
  3641. */
  3642. ItemSet.prototype._onRemove = function(ids) {
  3643. this._toQueue(ids, 'remove');
  3644. };
  3645. /**
  3646. * Put items in the queue to be added/updated/remove
  3647. * @param {Number[]} ids
  3648. * @param {String} action can be 'add', 'update', 'remove'
  3649. */
  3650. ItemSet.prototype._toQueue = function (ids, action) {
  3651. var items = this.items;
  3652. var queue = this.queue;
  3653. ids.forEach(function (id) {
  3654. var entry = queue[id];
  3655. if (entry) {
  3656. // already queued, update the action of the entry
  3657. entry.action = action;
  3658. }
  3659. else {
  3660. // not yet queued, add an entry to the queue
  3661. queue[id] = {
  3662. item: items[id] || null,
  3663. action: action
  3664. };
  3665. }
  3666. });
  3667. if (this.controller) {
  3668. //this.requestReflow();
  3669. this.requestRepaint();
  3670. }
  3671. };
  3672. /**
  3673. * Calculate the factor and offset to convert a position on screen to the
  3674. * corresponding date and vice versa.
  3675. * After the method _updateConversion is executed once, the methods toTime
  3676. * and toScreen can be used.
  3677. * @private
  3678. */
  3679. ItemSet.prototype._updateConversion = function() {
  3680. var range = this.range;
  3681. if (!range) {
  3682. throw new Error('No range configured');
  3683. }
  3684. if (range.conversion) {
  3685. this.conversion = range.conversion(this.width);
  3686. }
  3687. else {
  3688. this.conversion = Range.conversion(range.start, range.end, this.width);
  3689. }
  3690. };
  3691. /**
  3692. * Convert a position on screen (pixels) to a datetime
  3693. * Before this method can be used, the method _updateConversion must be
  3694. * executed once.
  3695. * @param {int} x Position on the screen in pixels
  3696. * @return {Date} time The datetime the corresponds with given position x
  3697. */
  3698. ItemSet.prototype.toTime = function(x) {
  3699. var conversion = this.conversion;
  3700. return new Date(x / conversion.factor + conversion.offset);
  3701. };
  3702. /**
  3703. * Convert a datetime (Date object) into a position on the screen
  3704. * Before this method can be used, the method _updateConversion must be
  3705. * executed once.
  3706. * @param {Date} time A date
  3707. * @return {int} x The position on the screen in pixels which corresponds
  3708. * with the given date.
  3709. */
  3710. ItemSet.prototype.toScreen = function(time) {
  3711. var conversion = this.conversion;
  3712. return (time.valueOf() - conversion.offset) * conversion.factor;
  3713. };
  3714. // exports
  3715. vis.component.ItemSet = ItemSet;
  3716. /**
  3717. * @constructor Item
  3718. * @param {ItemSet} parent
  3719. * @param {Object} data Object containing (optional) parameters type,
  3720. * start, end, content, group, className.
  3721. * @param {Object} [options] Options to set initial property values
  3722. * // TODO: describe available options
  3723. */
  3724. function Item (parent, data, options) {
  3725. this.parent = parent;
  3726. this.data = data;
  3727. this.selected = false;
  3728. this.visible = true;
  3729. this.dom = null;
  3730. this.options = options;
  3731. }
  3732. Item.prototype = new Component();
  3733. /**
  3734. * Select current item
  3735. */
  3736. Item.prototype.select = function () {
  3737. this.selected = true;
  3738. };
  3739. /**
  3740. * Unselect current item
  3741. */
  3742. Item.prototype.unselect = function () {
  3743. this.selected = false;
  3744. };
  3745. // exports
  3746. vis.component.item.Item = Item;
  3747. /**
  3748. * @constructor ItemBox
  3749. * @extends Item
  3750. * @param {ItemSet} parent
  3751. * @param {Object} data Object containing parameters start
  3752. * content, className.
  3753. * @param {Object} [options] Options to set initial property values
  3754. * // TODO: describe available options
  3755. */
  3756. function ItemBox (parent, data, options) {
  3757. this.props = {
  3758. dot: {
  3759. left: 0,
  3760. top: 0,
  3761. width: 0,
  3762. height: 0
  3763. },
  3764. line: {
  3765. top: 0,
  3766. left: 0,
  3767. width: 0,
  3768. height: 0
  3769. }
  3770. };
  3771. Item.call(this, parent, data, options);
  3772. }
  3773. ItemBox.prototype = new Item (null, null);
  3774. /**
  3775. * Select the item
  3776. * @override
  3777. */
  3778. ItemBox.prototype.select = function () {
  3779. this.selected = true;
  3780. // TODO: select and unselect
  3781. };
  3782. /**
  3783. * Unselect the item
  3784. * @override
  3785. */
  3786. ItemBox.prototype.unselect = function () {
  3787. this.selected = false;
  3788. // TODO: select and unselect
  3789. };
  3790. /**
  3791. * Repaint the item
  3792. * @return {Boolean} changed
  3793. */
  3794. ItemBox.prototype.repaint = function () {
  3795. // TODO: make an efficient repaint
  3796. var changed = false;
  3797. var dom = this.dom;
  3798. if (this.visible) {
  3799. if (!dom) {
  3800. this._create();
  3801. changed = true;
  3802. }
  3803. dom = this.dom;
  3804. if (dom) {
  3805. if (!this.options && !this.parent) {
  3806. throw new Error('Cannot repaint item: no parent attached');
  3807. }
  3808. var parentContainer = this.parent.getContainer();
  3809. if (!parentContainer) {
  3810. throw new Error('Cannot repaint time axis: parent has no container element');
  3811. }
  3812. if (!dom.box.parentNode) {
  3813. parentContainer.appendChild(dom.box);
  3814. changed = true;
  3815. }
  3816. if (!dom.line.parentNode) {
  3817. parentContainer.appendChild(dom.line);
  3818. changed = true;
  3819. }
  3820. if (!dom.dot.parentNode) {
  3821. parentContainer.appendChild(dom.dot);
  3822. changed = true;
  3823. }
  3824. // update contents
  3825. if (this.data.content != this.content) {
  3826. this.content = this.data.content;
  3827. if (this.content instanceof Element) {
  3828. dom.content.innerHTML = '';
  3829. dom.content.appendChild(this.content);
  3830. }
  3831. else if (this.data.content != undefined) {
  3832. dom.content.innerHTML = this.content;
  3833. }
  3834. else {
  3835. throw new Error('Property "content" missing in item ' + this.data.id);
  3836. }
  3837. changed = true;
  3838. }
  3839. // update class
  3840. var className = (this.data.className? ' ' + this.data.className : '') +
  3841. (this.selected ? ' selected' : '');
  3842. if (this.className != className) {
  3843. this.className = className;
  3844. dom.box.className = 'item box' + className;
  3845. dom.line.className = 'item line' + className;
  3846. dom.dot.className = 'item dot' + className;
  3847. changed = true;
  3848. }
  3849. }
  3850. }
  3851. else {
  3852. // hide when visible
  3853. if (dom) {
  3854. if (dom.box.parentNode) {
  3855. dom.box.parentNode.removeChild(dom.box);
  3856. changed = true;
  3857. }
  3858. if (dom.line.parentNode) {
  3859. dom.line.parentNode.removeChild(dom.line);
  3860. changed = true;
  3861. }
  3862. if (dom.dot.parentNode) {
  3863. dom.dot.parentNode.removeChild(dom.dot);
  3864. changed = true;
  3865. }
  3866. }
  3867. }
  3868. return changed;
  3869. };
  3870. /**
  3871. * Reflow the item: calculate its actual size and position from the DOM
  3872. * @return {boolean} resized returns true if the axis is resized
  3873. * @override
  3874. */
  3875. ItemBox.prototype.reflow = function () {
  3876. if (this.data.start == undefined) {
  3877. throw new Error('Property "start" missing in item ' + this.data.id);
  3878. }
  3879. var update = util.updateProperty,
  3880. dom = this.dom,
  3881. props = this.props,
  3882. options = this.options,
  3883. start = this.parent.toScreen(this.data.start),
  3884. align = options && options.align,
  3885. orientation = options.orientation,
  3886. changed = 0,
  3887. top,
  3888. left;
  3889. if (dom) {
  3890. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  3891. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  3892. changed += update(props.line, 'width', dom.line.offsetWidth);
  3893. changed += update(props.line, 'width', dom.line.offsetWidth);
  3894. changed += update(this, 'width', dom.box.offsetWidth);
  3895. changed += update(this, 'height', dom.box.offsetHeight);
  3896. if (align == 'right') {
  3897. left = start - this.width;
  3898. }
  3899. else if (align == 'left') {
  3900. left = start;
  3901. }
  3902. else {
  3903. // default or 'center'
  3904. left = start - this.width / 2;
  3905. }
  3906. changed += update(this, 'left', left);
  3907. changed += update(props.line, 'left', start - props.line.width / 2);
  3908. changed += update(props.dot, 'left', start - props.dot.width / 2);
  3909. if (orientation == 'top') {
  3910. top = options.margin.axis;
  3911. changed += update(this, 'top', top);
  3912. changed += update(props.line, 'top', 0);
  3913. changed += update(props.line, 'height', top);
  3914. changed += update(props.dot, 'top', -props.dot.height / 2);
  3915. }
  3916. else {
  3917. // default or 'bottom'
  3918. var parentHeight = this.parent.height;
  3919. top = parentHeight - this.height - options.margin.axis;
  3920. changed += update(this, 'top', top);
  3921. changed += update(props.line, 'top', top + this.height);
  3922. changed += update(props.line, 'height', Math.max(options.margin.axis, 0));
  3923. changed += update(props.dot, 'top', parentHeight - props.dot.height / 2);
  3924. }
  3925. }
  3926. else {
  3927. changed += 1;
  3928. }
  3929. return (changed > 0);
  3930. };
  3931. /**
  3932. * Create an items DOM
  3933. * @private
  3934. */
  3935. ItemBox.prototype._create = function () {
  3936. var dom = this.dom;
  3937. if (!dom) {
  3938. this.dom = dom = {};
  3939. // create the box
  3940. dom.box = document.createElement('DIV');
  3941. // className is updated in repaint()
  3942. // contents box (inside the background box). used for making margins
  3943. dom.content = document.createElement('DIV');
  3944. dom.content.className = 'content';
  3945. dom.box.appendChild(dom.content);
  3946. // line to axis
  3947. dom.line = document.createElement('DIV');
  3948. dom.line.className = 'line';
  3949. // dot on axis
  3950. dom.dot = document.createElement('DIV');
  3951. dom.dot.className = 'dot';
  3952. }
  3953. };
  3954. /**
  3955. * Reposition the item, recalculate its left, top, and width, using the current
  3956. * range and size of the items itemset
  3957. * @override
  3958. */
  3959. ItemBox.prototype.reposition = function () {
  3960. var dom = this.dom,
  3961. props = this.props,
  3962. orientation = this.options.orientation;
  3963. if (dom) {
  3964. var box = dom.box,
  3965. line = dom.line,
  3966. dot = dom.dot;
  3967. box.style.left = this.left + 'px';
  3968. box.style.top = this.top + 'px';
  3969. line.style.left = props.line.left + 'px';
  3970. if (orientation == 'top') {
  3971. line.style.top = 0 + 'px';
  3972. line.style.height = this.top + 'px';
  3973. }
  3974. else {
  3975. // orientation 'bottom'
  3976. line.style.top = props.line.top + 'px';
  3977. line.style.top = (this.top + this.height) + 'px';
  3978. line.style.height = Math.max(props.dot.top - this.top - this.height, 0) + 'px';
  3979. }
  3980. dot.style.left = props.dot.left + 'px';
  3981. dot.style.top = props.dot.top + 'px';
  3982. }
  3983. };
  3984. // exports
  3985. vis.component.item.box = ItemBox;
  3986. /**
  3987. * @constructor ItemPoint
  3988. * @extends Item
  3989. * @param {ItemSet} parent
  3990. * @param {Object} data Object containing parameters start
  3991. * content, className.
  3992. * @param {Object} [options] Options to set initial property values
  3993. * // TODO: describe available options
  3994. */
  3995. function ItemPoint (parent, data, options) {
  3996. this.props = {
  3997. dot: {
  3998. top: 0,
  3999. width: 0,
  4000. height: 0
  4001. },
  4002. content: {
  4003. height: 0,
  4004. marginLeft: 0
  4005. }
  4006. };
  4007. Item.call(this, parent, data, options);
  4008. }
  4009. ItemPoint.prototype = new Item (null, null);
  4010. /**
  4011. * Select the item
  4012. * @override
  4013. */
  4014. ItemPoint.prototype.select = function () {
  4015. this.selected = true;
  4016. // TODO: select and unselect
  4017. };
  4018. /**
  4019. * Unselect the item
  4020. * @override
  4021. */
  4022. ItemPoint.prototype.unselect = function () {
  4023. this.selected = false;
  4024. // TODO: select and unselect
  4025. };
  4026. /**
  4027. * Repaint the item
  4028. * @return {Boolean} changed
  4029. */
  4030. ItemPoint.prototype.repaint = function () {
  4031. // TODO: make an efficient repaint
  4032. var changed = false;
  4033. var dom = this.dom;
  4034. if (this.visible) {
  4035. if (!dom) {
  4036. this._create();
  4037. changed = true;
  4038. }
  4039. dom = this.dom;
  4040. if (dom) {
  4041. if (!this.options && !this.options.parent) {
  4042. throw new Error('Cannot repaint item: no parent attached');
  4043. }
  4044. var parentContainer = this.parent.getContainer();
  4045. if (!parentContainer) {
  4046. throw new Error('Cannot repaint time axis: parent has no container element');
  4047. }
  4048. if (!dom.point.parentNode) {
  4049. parentContainer.appendChild(dom.point);
  4050. changed = true;
  4051. }
  4052. // update contents
  4053. if (this.data.content != this.content) {
  4054. this.content = this.data.content;
  4055. if (this.content instanceof Element) {
  4056. dom.content.innerHTML = '';
  4057. dom.content.appendChild(this.content);
  4058. }
  4059. else if (this.data.content != undefined) {
  4060. dom.content.innerHTML = this.content;
  4061. }
  4062. else {
  4063. throw new Error('Property "content" missing in item ' + this.data.id);
  4064. }
  4065. changed = true;
  4066. }
  4067. // update class
  4068. var className = (this.data.className? ' ' + this.data.className : '') +
  4069. (this.selected ? ' selected' : '');
  4070. if (this.className != className) {
  4071. this.className = className;
  4072. dom.point.className = 'item point' + className;
  4073. changed = true;
  4074. }
  4075. }
  4076. }
  4077. else {
  4078. // hide when visible
  4079. if (dom) {
  4080. if (dom.point.parentNode) {
  4081. dom.point.parentNode.removeChild(dom.point);
  4082. changed = true;
  4083. }
  4084. }
  4085. }
  4086. return changed;
  4087. };
  4088. /**
  4089. * Reflow the item: calculate its actual size from the DOM
  4090. * @return {boolean} resized returns true if the axis is resized
  4091. * @override
  4092. */
  4093. ItemPoint.prototype.reflow = function () {
  4094. if (this.data.start == undefined) {
  4095. throw new Error('Property "start" missing in item ' + this.data.id);
  4096. }
  4097. var update = util.updateProperty,
  4098. dom = this.dom,
  4099. props = this.props,
  4100. options = this.options,
  4101. orientation = options.orientation,
  4102. start = this.parent.toScreen(this.data.start),
  4103. changed = 0,
  4104. top;
  4105. if (dom) {
  4106. changed += update(this, 'width', dom.point.offsetWidth);
  4107. changed += update(this, 'height', dom.point.offsetHeight);
  4108. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4109. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4110. changed += update(props.content, 'height', dom.content.offsetHeight);
  4111. if (orientation == 'top') {
  4112. top = options.margin.axis;
  4113. }
  4114. else {
  4115. // default or 'bottom'
  4116. var parentHeight = this.parent.height;
  4117. top = Math.max(parentHeight - this.height - options.margin.axis, 0);
  4118. }
  4119. changed += update(this, 'top', top);
  4120. changed += update(this, 'left', start - props.dot.width / 2);
  4121. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  4122. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  4123. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  4124. }
  4125. else {
  4126. changed += 1;
  4127. }
  4128. return (changed > 0);
  4129. };
  4130. /**
  4131. * Create an items DOM
  4132. * @private
  4133. */
  4134. ItemPoint.prototype._create = function () {
  4135. var dom = this.dom;
  4136. if (!dom) {
  4137. this.dom = dom = {};
  4138. // background box
  4139. dom.point = document.createElement('div');
  4140. // className is updated in repaint()
  4141. // contents box, right from the dot
  4142. dom.content = document.createElement('div');
  4143. dom.content.className = 'content';
  4144. dom.point.appendChild(dom.content);
  4145. // dot at start
  4146. dom.dot = document.createElement('div');
  4147. dom.dot.className = 'dot';
  4148. dom.point.appendChild(dom.dot);
  4149. }
  4150. };
  4151. /**
  4152. * Reposition the item, recalculate its left, top, and width, using the current
  4153. * range and size of the items itemset
  4154. * @override
  4155. */
  4156. ItemPoint.prototype.reposition = function () {
  4157. var dom = this.dom,
  4158. props = this.props;
  4159. if (dom) {
  4160. dom.point.style.top = this.top + 'px';
  4161. dom.point.style.left = this.left + 'px';
  4162. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  4163. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  4164. dom.dot.style.top = props.dot.top + 'px';
  4165. }
  4166. };
  4167. // exports
  4168. vis.component.item.point = ItemPoint;
  4169. /**
  4170. * @constructor ItemRange
  4171. * @extends Item
  4172. * @param {ItemSet} parent
  4173. * @param {Object} data Object containing parameters start, end
  4174. * content, className.
  4175. * @param {Object} [options] Options to set initial property values
  4176. * // TODO: describe available options
  4177. */
  4178. function ItemRange (parent, data, options) {
  4179. this.props = {
  4180. content: {
  4181. left: 0,
  4182. width: 0
  4183. }
  4184. };
  4185. Item.call(this, parent, data, options);
  4186. }
  4187. ItemRange.prototype = new Item (null, null);
  4188. /**
  4189. * Select the item
  4190. * @override
  4191. */
  4192. ItemRange.prototype.select = function () {
  4193. this.selected = true;
  4194. // TODO: select and unselect
  4195. };
  4196. /**
  4197. * Unselect the item
  4198. * @override
  4199. */
  4200. ItemRange.prototype.unselect = function () {
  4201. this.selected = false;
  4202. // TODO: select and unselect
  4203. };
  4204. /**
  4205. * Repaint the item
  4206. * @return {Boolean} changed
  4207. */
  4208. ItemRange.prototype.repaint = function () {
  4209. // TODO: make an efficient repaint
  4210. var changed = false;
  4211. var dom = this.dom;
  4212. if (this.visible) {
  4213. if (!dom) {
  4214. this._create();
  4215. changed = true;
  4216. }
  4217. dom = this.dom;
  4218. if (dom) {
  4219. if (!this.options && !this.options.parent) {
  4220. throw new Error('Cannot repaint item: no parent attached');
  4221. }
  4222. var parentContainer = this.parent.getContainer();
  4223. if (!parentContainer) {
  4224. throw new Error('Cannot repaint time axis: parent has no container element');
  4225. }
  4226. if (!dom.box.parentNode) {
  4227. parentContainer.appendChild(dom.box);
  4228. changed = true;
  4229. }
  4230. // update content
  4231. if (this.data.content != this.content) {
  4232. this.content = this.data.content;
  4233. if (this.content instanceof Element) {
  4234. dom.content.innerHTML = '';
  4235. dom.content.appendChild(this.content);
  4236. }
  4237. else if (this.data.content != undefined) {
  4238. dom.content.innerHTML = this.content;
  4239. }
  4240. else {
  4241. throw new Error('Property "content" missing in item ' + this.data.id);
  4242. }
  4243. changed = true;
  4244. }
  4245. // update class
  4246. var className = this.data.className ? ('' + this.data.className) : '';
  4247. if (this.className != className) {
  4248. this.className = className;
  4249. dom.box.className = 'item range' + className;
  4250. changed = true;
  4251. }
  4252. }
  4253. }
  4254. else {
  4255. // hide when visible
  4256. if (dom) {
  4257. if (dom.box.parentNode) {
  4258. dom.box.parentNode.removeChild(dom.box);
  4259. changed = true;
  4260. }
  4261. }
  4262. }
  4263. return changed;
  4264. };
  4265. /**
  4266. * Reflow the item: calculate its actual size from the DOM
  4267. * @return {boolean} resized returns true if the axis is resized
  4268. * @override
  4269. */
  4270. ItemRange.prototype.reflow = function () {
  4271. if (this.data.start == undefined) {
  4272. throw new Error('Property "start" missing in item ' + this.data.id);
  4273. }
  4274. if (this.data.end == undefined) {
  4275. throw new Error('Property "end" missing in item ' + this.data.id);
  4276. }
  4277. var dom = this.dom,
  4278. props = this.props,
  4279. options = this.options,
  4280. parent = this.parent,
  4281. start = parent.toScreen(this.data.start),
  4282. end = parent.toScreen(this.data.end),
  4283. changed = 0;
  4284. if (dom) {
  4285. var update = util.updateProperty,
  4286. box = dom.box,
  4287. parentWidth = parent.width,
  4288. orientation = options.orientation,
  4289. contentLeft,
  4290. top;
  4291. changed += update(props.content, 'width', dom.content.offsetWidth);
  4292. changed += update(this, 'height', box.offsetHeight);
  4293. // limit the width of the this, as browsers cannot draw very wide divs
  4294. if (start < -parentWidth) {
  4295. start = -parentWidth;
  4296. }
  4297. if (end > 2 * parentWidth) {
  4298. end = 2 * parentWidth;
  4299. }
  4300. // when range exceeds left of the window, position the contents at the left of the visible area
  4301. if (start < 0) {
  4302. contentLeft = Math.min(-start,
  4303. (end - start - props.content.width - 2 * options.padding));
  4304. // TODO: remove the need for options.padding. it's terrible.
  4305. }
  4306. else {
  4307. contentLeft = 0;
  4308. }
  4309. changed += update(props.content, 'left', contentLeft);
  4310. if (orientation == 'top') {
  4311. top = options.margin.axis;
  4312. changed += update(this, 'top', top);
  4313. }
  4314. else {
  4315. // default or 'bottom'
  4316. top = parent.height - this.height - options.margin.axis;
  4317. changed += update(this, 'top', top);
  4318. }
  4319. changed += update(this, 'left', start);
  4320. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  4321. }
  4322. else {
  4323. changed += 1;
  4324. }
  4325. return (changed > 0);
  4326. };
  4327. /**
  4328. * Create an items DOM
  4329. * @private
  4330. */
  4331. ItemRange.prototype._create = function () {
  4332. var dom = this.dom;
  4333. if (!dom) {
  4334. this.dom = dom = {};
  4335. // background box
  4336. dom.box = document.createElement('div');
  4337. // className is updated in repaint()
  4338. // contents box
  4339. dom.content = document.createElement('div');
  4340. dom.content.className = 'content';
  4341. dom.box.appendChild(dom.content);
  4342. }
  4343. };
  4344. /**
  4345. * Reposition the item, recalculate its left, top, and width, using the current
  4346. * range and size of the items itemset
  4347. * @override
  4348. */
  4349. ItemRange.prototype.reposition = function () {
  4350. var dom = this.dom,
  4351. props = this.props;
  4352. if (dom) {
  4353. dom.box.style.top = this.top + 'px';
  4354. dom.box.style.left = this.left + 'px';
  4355. dom.box.style.width = this.width + 'px';
  4356. dom.content.style.left = props.content.left + 'px';
  4357. }
  4358. };
  4359. // exports
  4360. vis.component.item.range = ItemRange;
  4361. /**
  4362. * Create a timeline visualization
  4363. * @param {HTMLElement} container
  4364. * @param {DataSet | Array | DataTable} [data]
  4365. * @param {Object} [options] See Timeline.setOptions for the available options.
  4366. * @constructor
  4367. */
  4368. function Timeline (container, data, options) {
  4369. var me = this;
  4370. this.options = {
  4371. orientation: 'bottom',
  4372. zoomMin: 10, // milliseconds
  4373. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  4374. moveable: true,
  4375. zoomable: true
  4376. };
  4377. // controller
  4378. this.controller = new Controller();
  4379. // main panel
  4380. if (!container) {
  4381. throw new Error('No container element provided');
  4382. }
  4383. this.main = new RootPanel(container, {
  4384. autoResize: false,
  4385. height: function () {
  4386. return me.timeaxis.height + me.itemset.height;
  4387. }
  4388. });
  4389. this.controller.add(this.main);
  4390. // range
  4391. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  4392. this.range = new Range({
  4393. start: now.clone().add('days', -3).valueOf(),
  4394. end: now.clone().add('days', 4).valueOf()
  4395. });
  4396. // TODO: reckon with options moveable and zoomable
  4397. this.range.subscribe(this.main, 'move', 'horizontal');
  4398. this.range.subscribe(this.main, 'zoom', 'horizontal');
  4399. this.range.on('rangechange', function () {
  4400. // TODO: fix the delay in reflow/repaint, does not feel snappy
  4401. me.controller.requestReflow();
  4402. });
  4403. this.range.on('rangechanged', function () {
  4404. me.controller.requestReflow();
  4405. });
  4406. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  4407. // time axis
  4408. this.timeaxis = new TimeAxis(this.main, null, {
  4409. orientation: this.options.orientation,
  4410. range: this.range
  4411. });
  4412. this.timeaxis.setRange(this.range);
  4413. this.controller.add(this.timeaxis);
  4414. // items panel
  4415. this.itemset = new ItemSet(this.main, [this.timeaxis], {
  4416. orientation: this.options.orientation
  4417. });
  4418. this.itemset.setRange(this.range);
  4419. // set data
  4420. if (data) {
  4421. this.setData(data);
  4422. }
  4423. this.controller.add(this.itemset);
  4424. this.setOptions(options);
  4425. }
  4426. /**
  4427. * Set options
  4428. * @param {Object} options TODO: describe the available options
  4429. */
  4430. Timeline.prototype.setOptions = function (options) {
  4431. util.extend(this.options, options);
  4432. // update options the timeaxis
  4433. this.timeaxis.setOptions(this.options);
  4434. // update options for the range
  4435. this.range.setOptions(this.options);
  4436. // update options the itemset
  4437. var top,
  4438. me = this;
  4439. if (this.options.orientation == 'top') {
  4440. top = function () {
  4441. return me.timeaxis.height;
  4442. }
  4443. }
  4444. else {
  4445. top = function () {
  4446. return me.main.height - me.timeaxis.height - me.itemset.height;
  4447. }
  4448. }
  4449. this.itemset.setOptions({
  4450. orientation: this.options.orientation,
  4451. top: top
  4452. });
  4453. this.controller.repaint();
  4454. };
  4455. /**
  4456. * Set data
  4457. * @param {DataSet | Array | DataTable} data
  4458. */
  4459. Timeline.prototype.setData = function(data) {
  4460. var dataset = this.itemset.data;
  4461. if (!dataset) {
  4462. // first load of data
  4463. this.itemset.setData(data);
  4464. // apply the data range as range
  4465. var dataRange = this.itemset.getDataRange();
  4466. // add 5% on both sides
  4467. var min = dataRange.min;
  4468. var max = dataRange.max;
  4469. if (min != null && max != null) {
  4470. var interval = (max.valueOf() - min.valueOf());
  4471. min = new Date(min.valueOf() - interval * 0.05);
  4472. max = new Date(max.valueOf() + interval * 0.05);
  4473. }
  4474. // apply range if there is a min or max available
  4475. if (min != null || max != null) {
  4476. this.range.setRange(min, max);
  4477. }
  4478. }
  4479. else {
  4480. // updated data
  4481. this.itemset.setData(data);
  4482. }
  4483. };
  4484. // exports
  4485. vis.Timeline = Timeline;
  4486. // moment.js
  4487. // version : 2.0.0
  4488. // author : Tim Wood
  4489. // license : MIT
  4490. // momentjs.com
  4491. (function (undefined) {
  4492. /************************************
  4493. Constants
  4494. ************************************/
  4495. var moment,
  4496. VERSION = "2.0.0",
  4497. round = Math.round, i,
  4498. // internal storage for language config files
  4499. languages = {},
  4500. // check for nodeJS
  4501. hasModule = (typeof module !== 'undefined' && module.exports),
  4502. // ASP.NET json date format regex
  4503. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  4504. // format tokens
  4505. 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,
  4506. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  4507. // parsing tokens
  4508. parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  4509. // parsing token regexes
  4510. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  4511. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  4512. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  4513. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  4514. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  4515. 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.
  4516. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  4517. parseTokenT = /T/i, // T (ISO seperator)
  4518. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  4519. // preliminary iso regex
  4520. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  4521. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  4522. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  4523. // iso time formats and regexes
  4524. isoTimes = [
  4525. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  4526. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  4527. ['HH:mm', /(T| )\d\d:\d\d/],
  4528. ['HH', /(T| )\d\d/]
  4529. ],
  4530. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  4531. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  4532. // getter and setter names
  4533. proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  4534. unitMillisecondFactors = {
  4535. 'Milliseconds' : 1,
  4536. 'Seconds' : 1e3,
  4537. 'Minutes' : 6e4,
  4538. 'Hours' : 36e5,
  4539. 'Days' : 864e5,
  4540. 'Months' : 2592e6,
  4541. 'Years' : 31536e6
  4542. },
  4543. // format function strings
  4544. formatFunctions = {},
  4545. // tokens to ordinalize and pad
  4546. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  4547. paddedTokens = 'M D H h m s w W'.split(' '),
  4548. formatTokenFunctions = {
  4549. M : function () {
  4550. return this.month() + 1;
  4551. },
  4552. MMM : function (format) {
  4553. return this.lang().monthsShort(this, format);
  4554. },
  4555. MMMM : function (format) {
  4556. return this.lang().months(this, format);
  4557. },
  4558. D : function () {
  4559. return this.date();
  4560. },
  4561. DDD : function () {
  4562. return this.dayOfYear();
  4563. },
  4564. d : function () {
  4565. return this.day();
  4566. },
  4567. dd : function (format) {
  4568. return this.lang().weekdaysMin(this, format);
  4569. },
  4570. ddd : function (format) {
  4571. return this.lang().weekdaysShort(this, format);
  4572. },
  4573. dddd : function (format) {
  4574. return this.lang().weekdays(this, format);
  4575. },
  4576. w : function () {
  4577. return this.week();
  4578. },
  4579. W : function () {
  4580. return this.isoWeek();
  4581. },
  4582. YY : function () {
  4583. return leftZeroFill(this.year() % 100, 2);
  4584. },
  4585. YYYY : function () {
  4586. return leftZeroFill(this.year(), 4);
  4587. },
  4588. YYYYY : function () {
  4589. return leftZeroFill(this.year(), 5);
  4590. },
  4591. a : function () {
  4592. return this.lang().meridiem(this.hours(), this.minutes(), true);
  4593. },
  4594. A : function () {
  4595. return this.lang().meridiem(this.hours(), this.minutes(), false);
  4596. },
  4597. H : function () {
  4598. return this.hours();
  4599. },
  4600. h : function () {
  4601. return this.hours() % 12 || 12;
  4602. },
  4603. m : function () {
  4604. return this.minutes();
  4605. },
  4606. s : function () {
  4607. return this.seconds();
  4608. },
  4609. S : function () {
  4610. return ~~(this.milliseconds() / 100);
  4611. },
  4612. SS : function () {
  4613. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  4614. },
  4615. SSS : function () {
  4616. return leftZeroFill(this.milliseconds(), 3);
  4617. },
  4618. Z : function () {
  4619. var a = -this.zone(),
  4620. b = "+";
  4621. if (a < 0) {
  4622. a = -a;
  4623. b = "-";
  4624. }
  4625. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  4626. },
  4627. ZZ : function () {
  4628. var a = -this.zone(),
  4629. b = "+";
  4630. if (a < 0) {
  4631. a = -a;
  4632. b = "-";
  4633. }
  4634. return b + leftZeroFill(~~(10 * a / 6), 4);
  4635. },
  4636. X : function () {
  4637. return this.unix();
  4638. }
  4639. };
  4640. function padToken(func, count) {
  4641. return function (a) {
  4642. return leftZeroFill(func.call(this, a), count);
  4643. };
  4644. }
  4645. function ordinalizeToken(func) {
  4646. return function (a) {
  4647. return this.lang().ordinal(func.call(this, a));
  4648. };
  4649. }
  4650. while (ordinalizeTokens.length) {
  4651. i = ordinalizeTokens.pop();
  4652. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  4653. }
  4654. while (paddedTokens.length) {
  4655. i = paddedTokens.pop();
  4656. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  4657. }
  4658. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  4659. /************************************
  4660. Constructors
  4661. ************************************/
  4662. function Language() {
  4663. }
  4664. // Moment prototype object
  4665. function Moment(config) {
  4666. extend(this, config);
  4667. }
  4668. // Duration Constructor
  4669. function Duration(duration) {
  4670. var data = this._data = {},
  4671. years = duration.years || duration.year || duration.y || 0,
  4672. months = duration.months || duration.month || duration.M || 0,
  4673. weeks = duration.weeks || duration.week || duration.w || 0,
  4674. days = duration.days || duration.day || duration.d || 0,
  4675. hours = duration.hours || duration.hour || duration.h || 0,
  4676. minutes = duration.minutes || duration.minute || duration.m || 0,
  4677. seconds = duration.seconds || duration.second || duration.s || 0,
  4678. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  4679. // representation for dateAddRemove
  4680. this._milliseconds = milliseconds +
  4681. seconds * 1e3 + // 1000
  4682. minutes * 6e4 + // 1000 * 60
  4683. hours * 36e5; // 1000 * 60 * 60
  4684. // Because of dateAddRemove treats 24 hours as different from a
  4685. // day when working around DST, we need to store them separately
  4686. this._days = days +
  4687. weeks * 7;
  4688. // It is impossible translate months into days without knowing
  4689. // which months you are are talking about, so we have to store
  4690. // it separately.
  4691. this._months = months +
  4692. years * 12;
  4693. // The following code bubbles up values, see the tests for
  4694. // examples of what that means.
  4695. data.milliseconds = milliseconds % 1000;
  4696. seconds += absRound(milliseconds / 1000);
  4697. data.seconds = seconds % 60;
  4698. minutes += absRound(seconds / 60);
  4699. data.minutes = minutes % 60;
  4700. hours += absRound(minutes / 60);
  4701. data.hours = hours % 24;
  4702. days += absRound(hours / 24);
  4703. days += weeks * 7;
  4704. data.days = days % 30;
  4705. months += absRound(days / 30);
  4706. data.months = months % 12;
  4707. years += absRound(months / 12);
  4708. data.years = years;
  4709. }
  4710. /************************************
  4711. Helpers
  4712. ************************************/
  4713. function extend(a, b) {
  4714. for (var i in b) {
  4715. if (b.hasOwnProperty(i)) {
  4716. a[i] = b[i];
  4717. }
  4718. }
  4719. return a;
  4720. }
  4721. function absRound(number) {
  4722. if (number < 0) {
  4723. return Math.ceil(number);
  4724. } else {
  4725. return Math.floor(number);
  4726. }
  4727. }
  4728. // left zero fill a number
  4729. // see http://jsperf.com/left-zero-filling for performance comparison
  4730. function leftZeroFill(number, targetLength) {
  4731. var output = number + '';
  4732. while (output.length < targetLength) {
  4733. output = '0' + output;
  4734. }
  4735. return output;
  4736. }
  4737. // helper function for _.addTime and _.subtractTime
  4738. function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  4739. var ms = duration._milliseconds,
  4740. d = duration._days,
  4741. M = duration._months,
  4742. currentDate;
  4743. if (ms) {
  4744. mom._d.setTime(+mom + ms * isAdding);
  4745. }
  4746. if (d) {
  4747. mom.date(mom.date() + d * isAdding);
  4748. }
  4749. if (M) {
  4750. currentDate = mom.date();
  4751. mom.date(1)
  4752. .month(mom.month() + M * isAdding)
  4753. .date(Math.min(currentDate, mom.daysInMonth()));
  4754. }
  4755. }
  4756. // check if is an array
  4757. function isArray(input) {
  4758. return Object.prototype.toString.call(input) === '[object Array]';
  4759. }
  4760. // compare two arrays, return the number of differences
  4761. function compareArrays(array1, array2) {
  4762. var len = Math.min(array1.length, array2.length),
  4763. lengthDiff = Math.abs(array1.length - array2.length),
  4764. diffs = 0,
  4765. i;
  4766. for (i = 0; i < len; i++) {
  4767. if (~~array1[i] !== ~~array2[i]) {
  4768. diffs++;
  4769. }
  4770. }
  4771. return diffs + lengthDiff;
  4772. }
  4773. /************************************
  4774. Languages
  4775. ************************************/
  4776. Language.prototype = {
  4777. set : function (config) {
  4778. var prop, i;
  4779. for (i in config) {
  4780. prop = config[i];
  4781. if (typeof prop === 'function') {
  4782. this[i] = prop;
  4783. } else {
  4784. this['_' + i] = prop;
  4785. }
  4786. }
  4787. },
  4788. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  4789. months : function (m) {
  4790. return this._months[m.month()];
  4791. },
  4792. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  4793. monthsShort : function (m) {
  4794. return this._monthsShort[m.month()];
  4795. },
  4796. monthsParse : function (monthName) {
  4797. var i, mom, regex, output;
  4798. if (!this._monthsParse) {
  4799. this._monthsParse = [];
  4800. }
  4801. for (i = 0; i < 12; i++) {
  4802. // make the regex if we don't have it already
  4803. if (!this._monthsParse[i]) {
  4804. mom = moment([2000, i]);
  4805. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  4806. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  4807. }
  4808. // test the regex
  4809. if (this._monthsParse[i].test(monthName)) {
  4810. return i;
  4811. }
  4812. }
  4813. },
  4814. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  4815. weekdays : function (m) {
  4816. return this._weekdays[m.day()];
  4817. },
  4818. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  4819. weekdaysShort : function (m) {
  4820. return this._weekdaysShort[m.day()];
  4821. },
  4822. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  4823. weekdaysMin : function (m) {
  4824. return this._weekdaysMin[m.day()];
  4825. },
  4826. _longDateFormat : {
  4827. LT : "h:mm A",
  4828. L : "MM/DD/YYYY",
  4829. LL : "MMMM D YYYY",
  4830. LLL : "MMMM D YYYY LT",
  4831. LLLL : "dddd, MMMM D YYYY LT"
  4832. },
  4833. longDateFormat : function (key) {
  4834. var output = this._longDateFormat[key];
  4835. if (!output && this._longDateFormat[key.toUpperCase()]) {
  4836. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  4837. return val.slice(1);
  4838. });
  4839. this._longDateFormat[key] = output;
  4840. }
  4841. return output;
  4842. },
  4843. meridiem : function (hours, minutes, isLower) {
  4844. if (hours > 11) {
  4845. return isLower ? 'pm' : 'PM';
  4846. } else {
  4847. return isLower ? 'am' : 'AM';
  4848. }
  4849. },
  4850. _calendar : {
  4851. sameDay : '[Today at] LT',
  4852. nextDay : '[Tomorrow at] LT',
  4853. nextWeek : 'dddd [at] LT',
  4854. lastDay : '[Yesterday at] LT',
  4855. lastWeek : '[last] dddd [at] LT',
  4856. sameElse : 'L'
  4857. },
  4858. calendar : function (key, mom) {
  4859. var output = this._calendar[key];
  4860. return typeof output === 'function' ? output.apply(mom) : output;
  4861. },
  4862. _relativeTime : {
  4863. future : "in %s",
  4864. past : "%s ago",
  4865. s : "a few seconds",
  4866. m : "a minute",
  4867. mm : "%d minutes",
  4868. h : "an hour",
  4869. hh : "%d hours",
  4870. d : "a day",
  4871. dd : "%d days",
  4872. M : "a month",
  4873. MM : "%d months",
  4874. y : "a year",
  4875. yy : "%d years"
  4876. },
  4877. relativeTime : function (number, withoutSuffix, string, isFuture) {
  4878. var output = this._relativeTime[string];
  4879. return (typeof output === 'function') ?
  4880. output(number, withoutSuffix, string, isFuture) :
  4881. output.replace(/%d/i, number);
  4882. },
  4883. pastFuture : function (diff, output) {
  4884. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  4885. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  4886. },
  4887. ordinal : function (number) {
  4888. return this._ordinal.replace("%d", number);
  4889. },
  4890. _ordinal : "%d",
  4891. preparse : function (string) {
  4892. return string;
  4893. },
  4894. postformat : function (string) {
  4895. return string;
  4896. },
  4897. week : function (mom) {
  4898. return weekOfYear(mom, this._week.dow, this._week.doy);
  4899. },
  4900. _week : {
  4901. dow : 0, // Sunday is the first day of the week.
  4902. doy : 6 // The week that contains Jan 1st is the first week of the year.
  4903. }
  4904. };
  4905. // Loads a language definition into the `languages` cache. The function
  4906. // takes a key and optionally values. If not in the browser and no values
  4907. // are provided, it will load the language file module. As a convenience,
  4908. // this function also returns the language values.
  4909. function loadLang(key, values) {
  4910. values.abbr = key;
  4911. if (!languages[key]) {
  4912. languages[key] = new Language();
  4913. }
  4914. languages[key].set(values);
  4915. return languages[key];
  4916. }
  4917. // Determines which language definition to use and returns it.
  4918. //
  4919. // With no parameters, it will return the global language. If you
  4920. // pass in a language key, such as 'en', it will return the
  4921. // definition for 'en', so long as 'en' has already been loaded using
  4922. // moment.lang.
  4923. function getLangDefinition(key) {
  4924. if (!key) {
  4925. return moment.fn._lang;
  4926. }
  4927. if (!languages[key] && hasModule) {
  4928. require('./lang/' + key);
  4929. }
  4930. return languages[key];
  4931. }
  4932. /************************************
  4933. Formatting
  4934. ************************************/
  4935. function removeFormattingTokens(input) {
  4936. if (input.match(/\[.*\]/)) {
  4937. return input.replace(/^\[|\]$/g, "");
  4938. }
  4939. return input.replace(/\\/g, "");
  4940. }
  4941. function makeFormatFunction(format) {
  4942. var array = format.match(formattingTokens), i, length;
  4943. for (i = 0, length = array.length; i < length; i++) {
  4944. if (formatTokenFunctions[array[i]]) {
  4945. array[i] = formatTokenFunctions[array[i]];
  4946. } else {
  4947. array[i] = removeFormattingTokens(array[i]);
  4948. }
  4949. }
  4950. return function (mom) {
  4951. var output = "";
  4952. for (i = 0; i < length; i++) {
  4953. output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  4954. }
  4955. return output;
  4956. };
  4957. }
  4958. // format date using native date object
  4959. function formatMoment(m, format) {
  4960. var i = 5;
  4961. function replaceLongDateFormatTokens(input) {
  4962. return m.lang().longDateFormat(input) || input;
  4963. }
  4964. while (i-- && localFormattingTokens.test(format)) {
  4965. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  4966. }
  4967. if (!formatFunctions[format]) {
  4968. formatFunctions[format] = makeFormatFunction(format);
  4969. }
  4970. return formatFunctions[format](m);
  4971. }
  4972. /************************************
  4973. Parsing
  4974. ************************************/
  4975. // get the regex to find the next token
  4976. function getParseRegexForToken(token) {
  4977. switch (token) {
  4978. case 'DDDD':
  4979. return parseTokenThreeDigits;
  4980. case 'YYYY':
  4981. return parseTokenFourDigits;
  4982. case 'YYYYY':
  4983. return parseTokenSixDigits;
  4984. case 'S':
  4985. case 'SS':
  4986. case 'SSS':
  4987. case 'DDD':
  4988. return parseTokenOneToThreeDigits;
  4989. case 'MMM':
  4990. case 'MMMM':
  4991. case 'dd':
  4992. case 'ddd':
  4993. case 'dddd':
  4994. case 'a':
  4995. case 'A':
  4996. return parseTokenWord;
  4997. case 'X':
  4998. return parseTokenTimestampMs;
  4999. case 'Z':
  5000. case 'ZZ':
  5001. return parseTokenTimezone;
  5002. case 'T':
  5003. return parseTokenT;
  5004. case 'MM':
  5005. case 'DD':
  5006. case 'YY':
  5007. case 'HH':
  5008. case 'hh':
  5009. case 'mm':
  5010. case 'ss':
  5011. case 'M':
  5012. case 'D':
  5013. case 'd':
  5014. case 'H':
  5015. case 'h':
  5016. case 'm':
  5017. case 's':
  5018. return parseTokenOneOrTwoDigits;
  5019. default :
  5020. return new RegExp(token.replace('\\', ''));
  5021. }
  5022. }
  5023. // function to convert string input to date
  5024. function addTimeToArrayFromToken(token, input, config) {
  5025. var a, b,
  5026. datePartArray = config._a;
  5027. switch (token) {
  5028. // MONTH
  5029. case 'M' : // fall through to MM
  5030. case 'MM' :
  5031. datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  5032. break;
  5033. case 'MMM' : // fall through to MMMM
  5034. case 'MMMM' :
  5035. a = getLangDefinition(config._l).monthsParse(input);
  5036. // if we didn't find a month name, mark the date as invalid.
  5037. if (a != null) {
  5038. datePartArray[1] = a;
  5039. } else {
  5040. config._isValid = false;
  5041. }
  5042. break;
  5043. // DAY OF MONTH
  5044. case 'D' : // fall through to DDDD
  5045. case 'DD' : // fall through to DDDD
  5046. case 'DDD' : // fall through to DDDD
  5047. case 'DDDD' :
  5048. if (input != null) {
  5049. datePartArray[2] = ~~input;
  5050. }
  5051. break;
  5052. // YEAR
  5053. case 'YY' :
  5054. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  5055. break;
  5056. case 'YYYY' :
  5057. case 'YYYYY' :
  5058. datePartArray[0] = ~~input;
  5059. break;
  5060. // AM / PM
  5061. case 'a' : // fall through to A
  5062. case 'A' :
  5063. config._isPm = ((input + '').toLowerCase() === 'pm');
  5064. break;
  5065. // 24 HOUR
  5066. case 'H' : // fall through to hh
  5067. case 'HH' : // fall through to hh
  5068. case 'h' : // fall through to hh
  5069. case 'hh' :
  5070. datePartArray[3] = ~~input;
  5071. break;
  5072. // MINUTE
  5073. case 'm' : // fall through to mm
  5074. case 'mm' :
  5075. datePartArray[4] = ~~input;
  5076. break;
  5077. // SECOND
  5078. case 's' : // fall through to ss
  5079. case 'ss' :
  5080. datePartArray[5] = ~~input;
  5081. break;
  5082. // MILLISECOND
  5083. case 'S' :
  5084. case 'SS' :
  5085. case 'SSS' :
  5086. datePartArray[6] = ~~ (('0.' + input) * 1000);
  5087. break;
  5088. // UNIX TIMESTAMP WITH MS
  5089. case 'X':
  5090. config._d = new Date(parseFloat(input) * 1000);
  5091. break;
  5092. // TIMEZONE
  5093. case 'Z' : // fall through to ZZ
  5094. case 'ZZ' :
  5095. config._useUTC = true;
  5096. a = (input + '').match(parseTimezoneChunker);
  5097. if (a && a[1]) {
  5098. config._tzh = ~~a[1];
  5099. }
  5100. if (a && a[2]) {
  5101. config._tzm = ~~a[2];
  5102. }
  5103. // reverse offsets
  5104. if (a && a[0] === '+') {
  5105. config._tzh = -config._tzh;
  5106. config._tzm = -config._tzm;
  5107. }
  5108. break;
  5109. }
  5110. // if the input is null, the date is not valid
  5111. if (input == null) {
  5112. config._isValid = false;
  5113. }
  5114. }
  5115. // convert an array to a date.
  5116. // the array should mirror the parameters below
  5117. // note: all values past the year are optional and will default to the lowest possible value.
  5118. // [year, month, day , hour, minute, second, millisecond]
  5119. function dateFromArray(config) {
  5120. var i, date, input = [];
  5121. if (config._d) {
  5122. return;
  5123. }
  5124. for (i = 0; i < 7; i++) {
  5125. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  5126. }
  5127. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  5128. input[3] += config._tzh || 0;
  5129. input[4] += config._tzm || 0;
  5130. date = new Date(0);
  5131. if (config._useUTC) {
  5132. date.setUTCFullYear(input[0], input[1], input[2]);
  5133. date.setUTCHours(input[3], input[4], input[5], input[6]);
  5134. } else {
  5135. date.setFullYear(input[0], input[1], input[2]);
  5136. date.setHours(input[3], input[4], input[5], input[6]);
  5137. }
  5138. config._d = date;
  5139. }
  5140. // date from string and format string
  5141. function makeDateFromStringAndFormat(config) {
  5142. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  5143. var tokens = config._f.match(formattingTokens),
  5144. string = config._i,
  5145. i, parsedInput;
  5146. config._a = [];
  5147. for (i = 0; i < tokens.length; i++) {
  5148. parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  5149. if (parsedInput) {
  5150. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  5151. }
  5152. // don't parse if its not a known token
  5153. if (formatTokenFunctions[tokens[i]]) {
  5154. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  5155. }
  5156. }
  5157. // handle am pm
  5158. if (config._isPm && config._a[3] < 12) {
  5159. config._a[3] += 12;
  5160. }
  5161. // if is 12 am, change hours to 0
  5162. if (config._isPm === false && config._a[3] === 12) {
  5163. config._a[3] = 0;
  5164. }
  5165. // return
  5166. dateFromArray(config);
  5167. }
  5168. // date from string and array of format strings
  5169. function makeDateFromStringAndArray(config) {
  5170. var tempConfig,
  5171. tempMoment,
  5172. bestMoment,
  5173. scoreToBeat = 99,
  5174. i,
  5175. currentDate,
  5176. currentScore;
  5177. while (config._f.length) {
  5178. tempConfig = extend({}, config);
  5179. tempConfig._f = config._f.pop();
  5180. makeDateFromStringAndFormat(tempConfig);
  5181. tempMoment = new Moment(tempConfig);
  5182. if (tempMoment.isValid()) {
  5183. bestMoment = tempMoment;
  5184. break;
  5185. }
  5186. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  5187. if (currentScore < scoreToBeat) {
  5188. scoreToBeat = currentScore;
  5189. bestMoment = tempMoment;
  5190. }
  5191. }
  5192. extend(config, bestMoment);
  5193. }
  5194. // date from iso format
  5195. function makeDateFromString(config) {
  5196. var i,
  5197. string = config._i;
  5198. if (isoRegex.exec(string)) {
  5199. config._f = 'YYYY-MM-DDT';
  5200. for (i = 0; i < 4; i++) {
  5201. if (isoTimes[i][1].exec(string)) {
  5202. config._f += isoTimes[i][0];
  5203. break;
  5204. }
  5205. }
  5206. if (parseTokenTimezone.exec(string)) {
  5207. config._f += " Z";
  5208. }
  5209. makeDateFromStringAndFormat(config);
  5210. } else {
  5211. config._d = new Date(string);
  5212. }
  5213. }
  5214. function makeDateFromInput(config) {
  5215. var input = config._i,
  5216. matched = aspNetJsonRegex.exec(input);
  5217. if (input === undefined) {
  5218. config._d = new Date();
  5219. } else if (matched) {
  5220. config._d = new Date(+matched[1]);
  5221. } else if (typeof input === 'string') {
  5222. makeDateFromString(config);
  5223. } else if (isArray(input)) {
  5224. config._a = input.slice(0);
  5225. dateFromArray(config);
  5226. } else {
  5227. config._d = input instanceof Date ? new Date(+input) : new Date(input);
  5228. }
  5229. }
  5230. /************************************
  5231. Relative Time
  5232. ************************************/
  5233. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  5234. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  5235. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  5236. }
  5237. function relativeTime(milliseconds, withoutSuffix, lang) {
  5238. var seconds = round(Math.abs(milliseconds) / 1000),
  5239. minutes = round(seconds / 60),
  5240. hours = round(minutes / 60),
  5241. days = round(hours / 24),
  5242. years = round(days / 365),
  5243. args = seconds < 45 && ['s', seconds] ||
  5244. minutes === 1 && ['m'] ||
  5245. minutes < 45 && ['mm', minutes] ||
  5246. hours === 1 && ['h'] ||
  5247. hours < 22 && ['hh', hours] ||
  5248. days === 1 && ['d'] ||
  5249. days <= 25 && ['dd', days] ||
  5250. days <= 45 && ['M'] ||
  5251. days < 345 && ['MM', round(days / 30)] ||
  5252. years === 1 && ['y'] || ['yy', years];
  5253. args[2] = withoutSuffix;
  5254. args[3] = milliseconds > 0;
  5255. args[4] = lang;
  5256. return substituteTimeAgo.apply({}, args);
  5257. }
  5258. /************************************
  5259. Week of Year
  5260. ************************************/
  5261. // firstDayOfWeek 0 = sun, 6 = sat
  5262. // the day of the week that starts the week
  5263. // (usually sunday or monday)
  5264. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  5265. // the first week is the week that contains the first
  5266. // of this day of the week
  5267. // (eg. ISO weeks use thursday (4))
  5268. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  5269. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  5270. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  5271. if (daysToDayOfWeek > end) {
  5272. daysToDayOfWeek -= 7;
  5273. }
  5274. if (daysToDayOfWeek < end - 7) {
  5275. daysToDayOfWeek += 7;
  5276. }
  5277. return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  5278. }
  5279. /************************************
  5280. Top Level Functions
  5281. ************************************/
  5282. function makeMoment(config) {
  5283. var input = config._i,
  5284. format = config._f;
  5285. if (input === null || input === '') {
  5286. return null;
  5287. }
  5288. if (typeof input === 'string') {
  5289. config._i = input = getLangDefinition().preparse(input);
  5290. }
  5291. if (moment.isMoment(input)) {
  5292. config = extend({}, input);
  5293. config._d = new Date(+input._d);
  5294. } else if (format) {
  5295. if (isArray(format)) {
  5296. makeDateFromStringAndArray(config);
  5297. } else {
  5298. makeDateFromStringAndFormat(config);
  5299. }
  5300. } else {
  5301. makeDateFromInput(config);
  5302. }
  5303. return new Moment(config);
  5304. }
  5305. moment = function (input, format, lang) {
  5306. return makeMoment({
  5307. _i : input,
  5308. _f : format,
  5309. _l : lang,
  5310. _isUTC : false
  5311. });
  5312. };
  5313. // creating with utc
  5314. moment.utc = function (input, format, lang) {
  5315. return makeMoment({
  5316. _useUTC : true,
  5317. _isUTC : true,
  5318. _l : lang,
  5319. _i : input,
  5320. _f : format
  5321. });
  5322. };
  5323. // creating with unix timestamp (in seconds)
  5324. moment.unix = function (input) {
  5325. return moment(input * 1000);
  5326. };
  5327. // duration
  5328. moment.duration = function (input, key) {
  5329. var isDuration = moment.isDuration(input),
  5330. isNumber = (typeof input === 'number'),
  5331. duration = (isDuration ? input._data : (isNumber ? {} : input)),
  5332. ret;
  5333. if (isNumber) {
  5334. if (key) {
  5335. duration[key] = input;
  5336. } else {
  5337. duration.milliseconds = input;
  5338. }
  5339. }
  5340. ret = new Duration(duration);
  5341. if (isDuration && input.hasOwnProperty('_lang')) {
  5342. ret._lang = input._lang;
  5343. }
  5344. return ret;
  5345. };
  5346. // version number
  5347. moment.version = VERSION;
  5348. // default format
  5349. moment.defaultFormat = isoFormat;
  5350. // This function will load languages and then set the global language. If
  5351. // no arguments are passed in, it will simply return the current global
  5352. // language key.
  5353. moment.lang = function (key, values) {
  5354. var i;
  5355. if (!key) {
  5356. return moment.fn._lang._abbr;
  5357. }
  5358. if (values) {
  5359. loadLang(key, values);
  5360. } else if (!languages[key]) {
  5361. getLangDefinition(key);
  5362. }
  5363. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  5364. };
  5365. // returns language data
  5366. moment.langData = function (key) {
  5367. if (key && key._lang && key._lang._abbr) {
  5368. key = key._lang._abbr;
  5369. }
  5370. return getLangDefinition(key);
  5371. };
  5372. // compare moment object
  5373. moment.isMoment = function (obj) {
  5374. return obj instanceof Moment;
  5375. };
  5376. // for typechecking Duration objects
  5377. moment.isDuration = function (obj) {
  5378. return obj instanceof Duration;
  5379. };
  5380. /************************************
  5381. Moment Prototype
  5382. ************************************/
  5383. moment.fn = Moment.prototype = {
  5384. clone : function () {
  5385. return moment(this);
  5386. },
  5387. valueOf : function () {
  5388. return +this._d;
  5389. },
  5390. unix : function () {
  5391. return Math.floor(+this._d / 1000);
  5392. },
  5393. toString : function () {
  5394. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  5395. },
  5396. toDate : function () {
  5397. return this._d;
  5398. },
  5399. toJSON : function () {
  5400. return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  5401. },
  5402. toArray : function () {
  5403. var m = this;
  5404. return [
  5405. m.year(),
  5406. m.month(),
  5407. m.date(),
  5408. m.hours(),
  5409. m.minutes(),
  5410. m.seconds(),
  5411. m.milliseconds()
  5412. ];
  5413. },
  5414. isValid : function () {
  5415. if (this._isValid == null) {
  5416. if (this._a) {
  5417. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  5418. } else {
  5419. this._isValid = !isNaN(this._d.getTime());
  5420. }
  5421. }
  5422. return !!this._isValid;
  5423. },
  5424. utc : function () {
  5425. this._isUTC = true;
  5426. return this;
  5427. },
  5428. local : function () {
  5429. this._isUTC = false;
  5430. return this;
  5431. },
  5432. format : function (inputString) {
  5433. var output = formatMoment(this, inputString || moment.defaultFormat);
  5434. return this.lang().postformat(output);
  5435. },
  5436. add : function (input, val) {
  5437. var dur;
  5438. // switch args to support add('s', 1) and add(1, 's')
  5439. if (typeof input === 'string') {
  5440. dur = moment.duration(+val, input);
  5441. } else {
  5442. dur = moment.duration(input, val);
  5443. }
  5444. addOrSubtractDurationFromMoment(this, dur, 1);
  5445. return this;
  5446. },
  5447. subtract : function (input, val) {
  5448. var dur;
  5449. // switch args to support subtract('s', 1) and subtract(1, 's')
  5450. if (typeof input === 'string') {
  5451. dur = moment.duration(+val, input);
  5452. } else {
  5453. dur = moment.duration(input, val);
  5454. }
  5455. addOrSubtractDurationFromMoment(this, dur, -1);
  5456. return this;
  5457. },
  5458. diff : function (input, units, asFloat) {
  5459. var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  5460. zoneDiff = (this.zone() - that.zone()) * 6e4,
  5461. diff, output;
  5462. if (units) {
  5463. // standardize on singular form
  5464. units = units.replace(/s$/, '');
  5465. }
  5466. if (units === 'year' || units === 'month') {
  5467. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  5468. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  5469. output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
  5470. if (units === 'year') {
  5471. output = output / 12;
  5472. }
  5473. } else {
  5474. diff = (this - that) - zoneDiff;
  5475. output = units === 'second' ? diff / 1e3 : // 1000
  5476. units === 'minute' ? diff / 6e4 : // 1000 * 60
  5477. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  5478. units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
  5479. units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
  5480. diff;
  5481. }
  5482. return asFloat ? output : absRound(output);
  5483. },
  5484. from : function (time, withoutSuffix) {
  5485. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  5486. },
  5487. fromNow : function (withoutSuffix) {
  5488. return this.from(moment(), withoutSuffix);
  5489. },
  5490. calendar : function () {
  5491. var diff = this.diff(moment().startOf('day'), 'days', true),
  5492. format = diff < -6 ? 'sameElse' :
  5493. diff < -1 ? 'lastWeek' :
  5494. diff < 0 ? 'lastDay' :
  5495. diff < 1 ? 'sameDay' :
  5496. diff < 2 ? 'nextDay' :
  5497. diff < 7 ? 'nextWeek' : 'sameElse';
  5498. return this.format(this.lang().calendar(format, this));
  5499. },
  5500. isLeapYear : function () {
  5501. var year = this.year();
  5502. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  5503. },
  5504. isDST : function () {
  5505. return (this.zone() < moment([this.year()]).zone() ||
  5506. this.zone() < moment([this.year(), 5]).zone());
  5507. },
  5508. day : function (input) {
  5509. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  5510. return input == null ? day :
  5511. this.add({ d : input - day });
  5512. },
  5513. startOf: function (units) {
  5514. units = units.replace(/s$/, '');
  5515. // the following switch intentionally omits break keywords
  5516. // to utilize falling through the cases.
  5517. switch (units) {
  5518. case 'year':
  5519. this.month(0);
  5520. /* falls through */
  5521. case 'month':
  5522. this.date(1);
  5523. /* falls through */
  5524. case 'week':
  5525. case 'day':
  5526. this.hours(0);
  5527. /* falls through */
  5528. case 'hour':
  5529. this.minutes(0);
  5530. /* falls through */
  5531. case 'minute':
  5532. this.seconds(0);
  5533. /* falls through */
  5534. case 'second':
  5535. this.milliseconds(0);
  5536. /* falls through */
  5537. }
  5538. // weeks are a special case
  5539. if (units === 'week') {
  5540. this.day(0);
  5541. }
  5542. return this;
  5543. },
  5544. endOf: function (units) {
  5545. return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
  5546. },
  5547. isAfter: function (input, units) {
  5548. units = typeof units !== 'undefined' ? units : 'millisecond';
  5549. return +this.clone().startOf(units) > +moment(input).startOf(units);
  5550. },
  5551. isBefore: function (input, units) {
  5552. units = typeof units !== 'undefined' ? units : 'millisecond';
  5553. return +this.clone().startOf(units) < +moment(input).startOf(units);
  5554. },
  5555. isSame: function (input, units) {
  5556. units = typeof units !== 'undefined' ? units : 'millisecond';
  5557. return +this.clone().startOf(units) === +moment(input).startOf(units);
  5558. },
  5559. zone : function () {
  5560. return this._isUTC ? 0 : this._d.getTimezoneOffset();
  5561. },
  5562. daysInMonth : function () {
  5563. return moment.utc([this.year(), this.month() + 1, 0]).date();
  5564. },
  5565. dayOfYear : function (input) {
  5566. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  5567. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  5568. },
  5569. isoWeek : function (input) {
  5570. var week = weekOfYear(this, 1, 4);
  5571. return input == null ? week : this.add("d", (input - week) * 7);
  5572. },
  5573. week : function (input) {
  5574. var week = this.lang().week(this);
  5575. return input == null ? week : this.add("d", (input - week) * 7);
  5576. },
  5577. // If passed a language key, it will set the language for this
  5578. // instance. Otherwise, it will return the language configuration
  5579. // variables for this instance.
  5580. lang : function (key) {
  5581. if (key === undefined) {
  5582. return this._lang;
  5583. } else {
  5584. this._lang = getLangDefinition(key);
  5585. return this;
  5586. }
  5587. }
  5588. };
  5589. // helper for adding shortcuts
  5590. function makeGetterAndSetter(name, key) {
  5591. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  5592. var utc = this._isUTC ? 'UTC' : '';
  5593. if (input != null) {
  5594. this._d['set' + utc + key](input);
  5595. return this;
  5596. } else {
  5597. return this._d['get' + utc + key]();
  5598. }
  5599. };
  5600. }
  5601. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  5602. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  5603. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  5604. }
  5605. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  5606. makeGetterAndSetter('year', 'FullYear');
  5607. // add plural methods
  5608. moment.fn.days = moment.fn.day;
  5609. moment.fn.weeks = moment.fn.week;
  5610. moment.fn.isoWeeks = moment.fn.isoWeek;
  5611. /************************************
  5612. Duration Prototype
  5613. ************************************/
  5614. moment.duration.fn = Duration.prototype = {
  5615. weeks : function () {
  5616. return absRound(this.days() / 7);
  5617. },
  5618. valueOf : function () {
  5619. return this._milliseconds +
  5620. this._days * 864e5 +
  5621. this._months * 2592e6;
  5622. },
  5623. humanize : function (withSuffix) {
  5624. var difference = +this,
  5625. output = relativeTime(difference, !withSuffix, this.lang());
  5626. if (withSuffix) {
  5627. output = this.lang().pastFuture(difference, output);
  5628. }
  5629. return this.lang().postformat(output);
  5630. },
  5631. lang : moment.fn.lang
  5632. };
  5633. function makeDurationGetter(name) {
  5634. moment.duration.fn[name] = function () {
  5635. return this._data[name];
  5636. };
  5637. }
  5638. function makeDurationAsGetter(name, factor) {
  5639. moment.duration.fn['as' + name] = function () {
  5640. return +this / factor;
  5641. };
  5642. }
  5643. for (i in unitMillisecondFactors) {
  5644. if (unitMillisecondFactors.hasOwnProperty(i)) {
  5645. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  5646. makeDurationGetter(i.toLowerCase());
  5647. }
  5648. }
  5649. makeDurationAsGetter('Weeks', 6048e5);
  5650. /************************************
  5651. Default Lang
  5652. ************************************/
  5653. // Set default language, other languages will inherit from English.
  5654. moment.lang('en', {
  5655. ordinal : function (number) {
  5656. var b = number % 10,
  5657. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  5658. (b === 1) ? 'st' :
  5659. (b === 2) ? 'nd' :
  5660. (b === 3) ? 'rd' : 'th';
  5661. return number + output;
  5662. }
  5663. });
  5664. /************************************
  5665. Exposing Moment
  5666. ************************************/
  5667. // CommonJS module is defined
  5668. if (hasModule) {
  5669. module.exports = moment;
  5670. }
  5671. /*global ender:false */
  5672. if (typeof ender === 'undefined') {
  5673. // here, `this` means `window` in the browser, or `global` on the server
  5674. // add `moment` as a global object via a string identifier,
  5675. // for Closure Compiler "advanced" mode
  5676. this['moment'] = moment;
  5677. }
  5678. /*global define:false */
  5679. if (typeof define === "function" && define.amd) {
  5680. define("moment", [], function () {
  5681. return moment;
  5682. });
  5683. }
  5684. }).call(this);
  5685. 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 .itemset {\n position: absolute;\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 z-index: -1;\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");
  5686. })();