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.

628 lines
21 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
  1. var moment = require('../module/moment');
  2. var DateUtil = require('./DateUtil');
  3. var util = require('../util');
  4. /**
  5. * @constructor TimeStep
  6. * The class TimeStep is an iterator for dates. You provide a start date and an
  7. * end date. The class itself determines the best scale (step size) based on the
  8. * provided start Date, end Date, and minimumStep.
  9. *
  10. * If minimumStep is provided, the step size is chosen as close as possible
  11. * to the minimumStep but larger than minimumStep. If minimumStep is not
  12. * provided, the scale is set to 1 DAY.
  13. * The minimumStep should correspond with the onscreen size of about 6 characters
  14. *
  15. * Alternatively, you can set a scale by hand.
  16. * After creation, you can initialize the class by executing first(). Then you
  17. * can iterate from the start date to the end date via next(). You can check if
  18. * the end date is reached with the function hasNext(). After each step, you can
  19. * retrieve the current date via getCurrent().
  20. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  21. * days, to years.
  22. *
  23. * Version: 1.2
  24. *
  25. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  26. * or new Date(2010, 9, 21, 23, 45, 00)
  27. * @param {Date} [end] The end date
  28. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  29. */
  30. function TimeStep(start, end, minimumStep, hiddenDates) {
  31. this.moment = moment;
  32. // variables
  33. this.current = this.moment();
  34. this._start = this.moment();
  35. this._end = this.moment();
  36. this.autoScale = true;
  37. this.scale = 'day';
  38. this.step = 1;
  39. // initialize the range
  40. this.setRange(start, end, minimumStep);
  41. // hidden Dates options
  42. this.switchedDay = false;
  43. this.switchedMonth = false;
  44. this.switchedYear = false;
  45. if (Array.isArray(hiddenDates)) {
  46. this.hiddenDates = hiddenDates;
  47. }
  48. else if (hiddenDates != undefined) {
  49. this.hiddenDates = [hiddenDates];
  50. }
  51. else {
  52. this.hiddenDates = [];
  53. }
  54. this.format = TimeStep.FORMAT; // default formatting
  55. }
  56. // Time formatting
  57. TimeStep.FORMAT = {
  58. minorLabels: {
  59. millisecond:'SSS',
  60. second: 's',
  61. minute: 'HH:mm',
  62. hour: 'HH:mm',
  63. weekday: 'ddd D',
  64. day: 'D',
  65. month: 'MMM',
  66. year: 'YYYY'
  67. },
  68. majorLabels: {
  69. millisecond:'HH:mm:ss',
  70. second: 'D MMMM HH:mm',
  71. minute: 'ddd D MMMM',
  72. hour: 'ddd D MMMM',
  73. weekday: 'MMMM YYYY',
  74. day: 'MMMM YYYY',
  75. month: 'YYYY',
  76. year: ''
  77. }
  78. };
  79. /**
  80. * Set custom constructor function for moment. Can be used to set dates
  81. * to UTC or to set a utcOffset.
  82. * @param {function} moment
  83. */
  84. TimeStep.prototype.setMoment = function (moment) {
  85. this.moment = moment;
  86. // update the date properties, can have a new utcOffset
  87. this.current = this.moment(this.current.valueOf());
  88. this._start = this.moment(this._start.valueOf());
  89. this._end = this.moment(this._end.valueOf());
  90. };
  91. /**
  92. * Set custom formatting for the minor an major labels of the TimeStep.
  93. * Both `minorLabels` and `majorLabels` are an Object with properties:
  94. * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'.
  95. * @param {{minorLabels: Object, majorLabels: Object}} format
  96. */
  97. TimeStep.prototype.setFormat = function (format) {
  98. var defaultFormat = util.deepExtend({}, TimeStep.FORMAT);
  99. this.format = util.deepExtend(defaultFormat, format);
  100. };
  101. /**
  102. * Set a new range
  103. * If minimumStep is provided, the step size is chosen as close as possible
  104. * to the minimumStep but larger than minimumStep. If minimumStep is not
  105. * provided, the scale is set to 1 DAY.
  106. * The minimumStep should correspond with the onscreen size of about 6 characters
  107. * @param {Date} [start] The start date and time.
  108. * @param {Date} [end] The end date and time.
  109. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  110. */
  111. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  112. if (!(start instanceof Date) || !(end instanceof Date)) {
  113. throw "No legal start or end date in method setRange";
  114. }
  115. this._start = (start != undefined) ? this.moment(start.valueOf()) : new Date();
  116. this._end = (end != undefined) ? this.moment(end.valueOf()) : new Date();
  117. if (this.autoScale) {
  118. this.setMinimumStep(minimumStep);
  119. }
  120. };
  121. /**
  122. * Set the range iterator to the start date.
  123. */
  124. TimeStep.prototype.start = function() {
  125. this.current = this._start.clone();
  126. this.roundToMinor();
  127. };
  128. /**
  129. * Round the current date to the first minor date value
  130. * This must be executed once when the current date is set to start Date
  131. */
  132. TimeStep.prototype.roundToMinor = function() {
  133. // round to floor
  134. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  135. // noinspection FallThroughInSwitchStatementJS
  136. switch (this.scale) {
  137. case 'year':
  138. this.current.year(this.step * Math.floor(this.current.year() / this.step));
  139. this.current.month(0);
  140. case 'month': this.current.date(1);
  141. case 'day': // intentional fall through
  142. case 'weekday': this.current.hours(0);
  143. case 'hour': this.current.minutes(0);
  144. case 'minute': this.current.seconds(0);
  145. case 'second': this.current.milliseconds(0);
  146. //case 'millisecond': // nothing to do for milliseconds
  147. }
  148. if (this.step != 1) {
  149. // round down to the first minor value that is a multiple of the current step size
  150. switch (this.scale) {
  151. case 'millisecond': this.current.subtract(this.current.milliseconds() % this.step, 'milliseconds'); break;
  152. case 'second': this.current.subtract(this.current.seconds() % this.step, 'seconds'); break;
  153. case 'minute': this.current.subtract(this.current.minutes() % this.step, 'minutes'); break;
  154. case 'hour': this.current.subtract(this.current.hours() % this.step, 'hours'); break;
  155. case 'weekday': // intentional fall through
  156. case 'day': this.current.subtract((this.current.date() - 1) % this.step, 'day'); break;
  157. case 'month': this.current.subtract(this.current.month() % this.step, 'month'); break;
  158. case 'year': this.current.subtract(this.current.year() % this.step, 'year'); break;
  159. default: break;
  160. }
  161. }
  162. };
  163. /**
  164. * Check if the there is a next step
  165. * @return {boolean} true if the current date has not passed the end date
  166. */
  167. TimeStep.prototype.hasNext = function () {
  168. return (this.current.valueOf() <= this._end.valueOf());
  169. };
  170. /**
  171. * Do the next step
  172. */
  173. TimeStep.prototype.next = function() {
  174. var prev = this.current.valueOf();
  175. // Two cases, needed to prevent issues with switching daylight savings
  176. // (end of March and end of October)
  177. if (this.current.month() < 6) {
  178. switch (this.scale) {
  179. case 'millisecond': this.current.add(this.step, 'millisecond'); break;
  180. case 'second': this.current.add(this.step, 'second'); break;
  181. case 'minute': this.current.add(this.step, 'minute'); break;
  182. case 'hour':
  183. this.current.add(this.step, 'hour');
  184. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  185. // TODO: is this still needed now we use the function of moment.js?
  186. this.current.subtract(this.current.hours() % this.step, 'hour');
  187. break;
  188. case 'weekday': // intentional fall through
  189. case 'day': this.current.add(this.step, 'day'); break;
  190. case 'month': this.current.add(this.step, 'month'); break;
  191. case 'year': this.current.add(this.step, 'year'); break;
  192. default: break;
  193. }
  194. }
  195. else {
  196. switch (this.scale) {
  197. case 'millisecond': this.current.add(this.step, 'millisecond'); break;
  198. case 'second': this.current.add(this.step, 'second'); break;
  199. case 'minute': this.current.add(this.step, 'minute'); break;
  200. case 'hour': this.current.add(this.step, 'hour'); break;
  201. case 'weekday': // intentional fall through
  202. case 'day': this.current.add(this.step, 'day'); break;
  203. case 'month': this.current.add(this.step, 'month'); break;
  204. case 'year': this.current.add(this.step, 'year'); break;
  205. default: break;
  206. }
  207. }
  208. if (this.step != 1) {
  209. // round down to the correct major value
  210. switch (this.scale) {
  211. case 'millisecond': if(this.current.milliseconds() < this.step) this.current.milliseconds(0); break;
  212. case 'second': if(this.current.seconds() < this.step) this.current.seconds(0); break;
  213. case 'minute': if(this.current.minutes() < this.step) this.current.minutes(0); break;
  214. case 'hour': if(this.current.hours() < this.step) this.current.hours(0); break;
  215. case 'weekday': // intentional fall through
  216. case 'day': if(this.current.date() < this.step+1) this.current.date(1); break;
  217. case 'month': if(this.current.month() < this.step) this.current.month(0); break;
  218. case 'year': break; // nothing to do for year
  219. default: break;
  220. }
  221. }
  222. // safety mechanism: if current time is still unchanged, move to the end
  223. if (this.current.valueOf() == prev) {
  224. this.current = this._end.clone();
  225. }
  226. // Reset switches for year, month and day. Will get set to true where appropriate in DateUtil.stepOverHiddenDates
  227. this.switchedDay = false;
  228. this.switchedMonth = false;
  229. this.switchedYear = false;
  230. DateUtil.stepOverHiddenDates(this.moment, this, prev);
  231. };
  232. /**
  233. * Get the current datetime
  234. * @return {Moment} current The current date
  235. */
  236. TimeStep.prototype.getCurrent = function() {
  237. return this.current;
  238. };
  239. /**
  240. * Set a custom scale. Autoscaling will be disabled.
  241. * For example setScale('minute', 5) will result
  242. * in minor steps of 5 minutes, and major steps of an hour.
  243. *
  244. * @param {{scale: string, step: number}} params
  245. * An object containing two properties:
  246. * - A string 'scale'. Choose from 'millisecond', 'second',
  247. * 'minute', 'hour', 'weekday', 'day', 'month', 'year'.
  248. * - A number 'step'. A step size, by default 1.
  249. * Choose for example 1, 2, 5, or 10.
  250. */
  251. TimeStep.prototype.setScale = function(params) {
  252. if (params && typeof params.scale == 'string') {
  253. this.scale = params.scale;
  254. this.step = params.step > 0 ? params.step : 1;
  255. this.autoScale = false;
  256. }
  257. };
  258. /**
  259. * Enable or disable autoscaling
  260. * @param {boolean} enable If true, autoascaling is set true
  261. */
  262. TimeStep.prototype.setAutoScale = function (enable) {
  263. this.autoScale = enable;
  264. };
  265. /**
  266. * Automatically determine the scale that bests fits the provided minimum step
  267. * @param {Number} [minimumStep] The minimum step size in milliseconds
  268. */
  269. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  270. if (minimumStep == undefined) {
  271. return;
  272. }
  273. //var b = asc + ds;
  274. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  275. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  276. var stepDay = (1000 * 60 * 60 * 24);
  277. var stepHour = (1000 * 60 * 60);
  278. var stepMinute = (1000 * 60);
  279. var stepSecond = (1000);
  280. var stepMillisecond= (1);
  281. // find the smallest step that is larger than the provided minimumStep
  282. if (stepYear*1000 > minimumStep) {this.scale = 'year'; this.step = 1000;}
  283. if (stepYear*500 > minimumStep) {this.scale = 'year'; this.step = 500;}
  284. if (stepYear*100 > minimumStep) {this.scale = 'year'; this.step = 100;}
  285. if (stepYear*50 > minimumStep) {this.scale = 'year'; this.step = 50;}
  286. if (stepYear*10 > minimumStep) {this.scale = 'year'; this.step = 10;}
  287. if (stepYear*5 > minimumStep) {this.scale = 'year'; this.step = 5;}
  288. if (stepYear > minimumStep) {this.scale = 'year'; this.step = 1;}
  289. if (stepMonth*3 > minimumStep) {this.scale = 'month'; this.step = 3;}
  290. if (stepMonth > minimumStep) {this.scale = 'month'; this.step = 1;}
  291. if (stepDay*5 > minimumStep) {this.scale = 'day'; this.step = 5;}
  292. if (stepDay*2 > minimumStep) {this.scale = 'day'; this.step = 2;}
  293. if (stepDay > minimumStep) {this.scale = 'day'; this.step = 1;}
  294. if (stepDay/2 > minimumStep) {this.scale = 'weekday'; this.step = 1;}
  295. if (stepHour*4 > minimumStep) {this.scale = 'hour'; this.step = 4;}
  296. if (stepHour > minimumStep) {this.scale = 'hour'; this.step = 1;}
  297. if (stepMinute*15 > minimumStep) {this.scale = 'minute'; this.step = 15;}
  298. if (stepMinute*10 > minimumStep) {this.scale = 'minute'; this.step = 10;}
  299. if (stepMinute*5 > minimumStep) {this.scale = 'minute'; this.step = 5;}
  300. if (stepMinute > minimumStep) {this.scale = 'minute'; this.step = 1;}
  301. if (stepSecond*15 > minimumStep) {this.scale = 'second'; this.step = 15;}
  302. if (stepSecond*10 > minimumStep) {this.scale = 'second'; this.step = 10;}
  303. if (stepSecond*5 > minimumStep) {this.scale = 'second'; this.step = 5;}
  304. if (stepSecond > minimumStep) {this.scale = 'second'; this.step = 1;}
  305. if (stepMillisecond*200 > minimumStep) {this.scale = 'millisecond'; this.step = 200;}
  306. if (stepMillisecond*100 > minimumStep) {this.scale = 'millisecond'; this.step = 100;}
  307. if (stepMillisecond*50 > minimumStep) {this.scale = 'millisecond'; this.step = 50;}
  308. if (stepMillisecond*10 > minimumStep) {this.scale = 'millisecond'; this.step = 10;}
  309. if (stepMillisecond*5 > minimumStep) {this.scale = 'millisecond'; this.step = 5;}
  310. if (stepMillisecond > minimumStep) {this.scale = 'millisecond'; this.step = 1;}
  311. };
  312. /**
  313. * Snap a date to a rounded value.
  314. * The snap intervals are dependent on the current scale and step.
  315. * Static function
  316. * @param {Date} date the date to be snapped.
  317. * @param {string} scale Current scale, can be 'millisecond', 'second',
  318. * 'minute', 'hour', 'weekday, 'day', 'month', 'year'.
  319. * @param {number} step Current step (1, 2, 4, 5, ...
  320. * @return {Date} snappedDate
  321. */
  322. TimeStep.snap = function(date, scale, step) {
  323. var clone = moment(date);
  324. if (scale == 'year') {
  325. var year = clone.year() + Math.round(clone.month() / 12);
  326. clone.year(Math.round(year / step) * step);
  327. clone.month(0);
  328. clone.date(0);
  329. clone.hours(0);
  330. clone.minutes(0);
  331. clone.seconds(0);
  332. clone.milliseconds(0);
  333. }
  334. else if (scale == 'month') {
  335. if (clone.date() > 15) {
  336. clone.date(1);
  337. clone.add(1, 'month');
  338. // important: first set Date to 1, after that change the month.
  339. }
  340. else {
  341. clone.date(1);
  342. }
  343. clone.hours(0);
  344. clone.minutes(0);
  345. clone.seconds(0);
  346. clone.milliseconds(0);
  347. }
  348. else if (scale == 'day') {
  349. //noinspection FallthroughInSwitchStatementJS
  350. switch (step) {
  351. case 5:
  352. case 2:
  353. clone.hours(Math.round(clone.hours() / 24) * 24); break;
  354. default:
  355. clone.hours(Math.round(clone.hours() / 12) * 12); break;
  356. }
  357. clone.minutes(0);
  358. clone.seconds(0);
  359. clone.milliseconds(0);
  360. }
  361. else if (scale == 'weekday') {
  362. //noinspection FallthroughInSwitchStatementJS
  363. switch (step) {
  364. case 5:
  365. case 2:
  366. clone.hours(Math.round(clone.hours() / 12) * 12); break;
  367. default:
  368. clone.hours(Math.round(clone.hours() / 6) * 6); break;
  369. }
  370. clone.minutes(0);
  371. clone.seconds(0);
  372. clone.milliseconds(0);
  373. }
  374. else if (scale == 'hour') {
  375. switch (step) {
  376. case 4:
  377. clone.minutes(Math.round(clone.minutes() / 60) * 60); break;
  378. default:
  379. clone.minutes(Math.round(clone.minutes() / 30) * 30); break;
  380. }
  381. clone.seconds(0);
  382. clone.milliseconds(0);
  383. } else if (scale == 'minute') {
  384. //noinspection FallthroughInSwitchStatementJS
  385. switch (step) {
  386. case 15:
  387. case 10:
  388. clone.minutes(Math.round(clone.minutes() / 5) * 5);
  389. clone.seconds(0);
  390. break;
  391. case 5:
  392. clone.seconds(Math.round(clone.seconds() / 60) * 60); break;
  393. default:
  394. clone.seconds(Math.round(clone.seconds() / 30) * 30); break;
  395. }
  396. clone.milliseconds(0);
  397. }
  398. else if (scale == 'second') {
  399. //noinspection FallthroughInSwitchStatementJS
  400. switch (step) {
  401. case 15:
  402. case 10:
  403. clone.seconds(Math.round(clone.seconds() / 5) * 5);
  404. clone.milliseconds(0);
  405. break;
  406. case 5:
  407. clone.milliseconds(Math.round(clone.milliseconds() / 1000) * 1000); break;
  408. default:
  409. clone.milliseconds(Math.round(clone.milliseconds() / 500) * 500); break;
  410. }
  411. }
  412. else if (scale == 'millisecond') {
  413. var _step = step > 5 ? step / 2 : 1;
  414. clone.milliseconds(Math.round(clone.milliseconds() / _step) * _step);
  415. }
  416. return clone;
  417. };
  418. /**
  419. * Check if the current value is a major value (for example when the step
  420. * is DAY, a major value is each first day of the MONTH)
  421. * @return {boolean} true if current date is major, else false.
  422. */
  423. TimeStep.prototype.isMajor = function() {
  424. if (this.switchedYear == true) {
  425. switch (this.scale) {
  426. case 'year':
  427. case 'month':
  428. case 'weekday':
  429. case 'day':
  430. case 'hour':
  431. case 'minute':
  432. case 'second':
  433. case 'millisecond':
  434. return true;
  435. default:
  436. return false;
  437. }
  438. }
  439. else if (this.switchedMonth == true) {
  440. switch (this.scale) {
  441. case 'weekday':
  442. case 'day':
  443. case 'hour':
  444. case 'minute':
  445. case 'second':
  446. case 'millisecond':
  447. return true;
  448. default:
  449. return false;
  450. }
  451. }
  452. else if (this.switchedDay == true) {
  453. switch (this.scale) {
  454. case 'millisecond':
  455. case 'second':
  456. case 'minute':
  457. case 'hour':
  458. return true;
  459. default:
  460. return false;
  461. }
  462. }
  463. var date = this.moment(this.current);
  464. switch (this.scale) {
  465. case 'millisecond':
  466. return (date.milliseconds() == 0);
  467. case 'second':
  468. return (date.seconds() == 0);
  469. case 'minute':
  470. return (date.hours() == 0) && (date.minutes() == 0);
  471. case 'hour':
  472. return (date.hours() == 0);
  473. case 'weekday': // intentional fall through
  474. case 'day':
  475. return (date.date() == 1);
  476. case 'month':
  477. return (date.month() == 0);
  478. case 'year':
  479. return false;
  480. default:
  481. return false;
  482. }
  483. };
  484. /**
  485. * Returns formatted text for the minor axislabel, depending on the current
  486. * date and the scale. For example when scale is MINUTE, the current time is
  487. * formatted as "hh:mm".
  488. * @param {Date} [date] custom date. if not provided, current date is taken
  489. */
  490. TimeStep.prototype.getLabelMinor = function(date) {
  491. if (date == undefined) {
  492. date = this.current;
  493. }
  494. var format = this.format.minorLabels[this.scale];
  495. return (format && format.length > 0) ? this.moment(date).format(format) : '';
  496. };
  497. /**
  498. * Returns formatted text for the major axis label, depending on the current
  499. * date and the scale. For example when scale is MINUTE, the major scale is
  500. * hours, and the hour will be formatted as "hh".
  501. * @param {Date} [date] custom date. if not provided, current date is taken
  502. */
  503. TimeStep.prototype.getLabelMajor = function(date) {
  504. if (date == undefined) {
  505. date = this.current;
  506. }
  507. var format = this.format.majorLabels[this.scale];
  508. return (format && format.length > 0) ? this.moment(date).format(format) : '';
  509. };
  510. TimeStep.prototype.getClassName = function() {
  511. var _moment = this.moment;
  512. var m = this.moment(this.current);
  513. var current = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function
  514. var step = this.step;
  515. function even(value) {
  516. return (value / step % 2 == 0) ? ' vis-even' : ' vis-odd';
  517. }
  518. function today(date) {
  519. if (date.isSame(new Date(), 'day')) {
  520. return ' vis-today';
  521. }
  522. if (date.isSame(_moment().add(1, 'day'), 'day')) {
  523. return ' vis-tomorrow';
  524. }
  525. if (date.isSame(_moment().add(-1, 'day'), 'day')) {
  526. return ' vis-yesterday';
  527. }
  528. return '';
  529. }
  530. function currentWeek(date) {
  531. return date.isSame(new Date(), 'week') ? ' vis-current-week' : '';
  532. }
  533. function currentMonth(date) {
  534. return date.isSame(new Date(), 'month') ? ' vis-current-month' : '';
  535. }
  536. function currentYear(date) {
  537. return date.isSame(new Date(), 'year') ? ' vis-current-year' : '';
  538. }
  539. switch (this.scale) {
  540. case 'millisecond':
  541. return even(current.milliseconds()).trim();
  542. case 'second':
  543. return even(current.seconds()).trim();
  544. case 'minute':
  545. return even(current.minutes()).trim();
  546. case 'hour':
  547. var hours = current.hours();
  548. if (this.step == 4) {
  549. hours = hours + '-h' + (hours + 4);
  550. }
  551. return 'vis-h' + hours + today(current) + even(current.hours());
  552. case 'weekday':
  553. return 'vis-' + current.format('dddd').toLowerCase() +
  554. today(current) + currentWeek(current) + even(current.date());
  555. case 'day':
  556. var day = current.date();
  557. var month = current.format('MMMM').toLowerCase();
  558. return 'vis-day' + day + ' vis-' + month + currentMonth(current) + even(day - 1) + (this.step <= 2 ? today(current) +' vis-' + current.format('dddd').toLowerCase() : '');
  559. case 'month':
  560. return 'vis-' + current.format('MMMM').toLowerCase() +
  561. currentMonth(current) + even(current.month());
  562. case 'year':
  563. var year = current.year();
  564. return 'vis-year' + year + currentYear(current)+ even(year);
  565. default:
  566. return '';
  567. }
  568. };
  569. module.exports = TimeStep;