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.

609 lines
19 KiB

11 years ago
11 years ago
11 years ago
8 years ago
8 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. var Emitter = require('emitter-component');
  2. var Hammer = require('../module/hammer');
  3. var moment = require('../module/moment');
  4. var util = require('../util');
  5. var DataSet = require('../DataSet');
  6. var DataView = require('../DataView');
  7. var Range = require('./Range');
  8. var Core = require('./Core');
  9. var TimeAxis = require('./component/TimeAxis');
  10. var CurrentTime = require('./component/CurrentTime');
  11. var CustomTime = require('./component/CustomTime');
  12. var ItemSet = require('./component/ItemSet');
  13. var printStyle = require('../shared/Validator').printStyle;
  14. var allOptions = require('./optionsTimeline').allOptions;
  15. var configureOptions = require('./optionsTimeline').configureOptions;
  16. var Configurator = require('../shared/Configurator').default;
  17. var Validator = require('../shared/Validator').default;
  18. /**
  19. * Create a timeline visualization
  20. * @param {HTMLElement} container
  21. * @param {vis.DataSet | vis.DataView | Array} [items]
  22. * @param {vis.DataSet | vis.DataView | Array} [groups]
  23. * @param {Object} [options] See Timeline.setOptions for the available options.
  24. * @constructor
  25. * @extends Core
  26. */
  27. function Timeline (container, items, groups, options) {
  28. if (!(this instanceof Timeline)) {
  29. throw new SyntaxError('Constructor must be called with the new operator');
  30. }
  31. // if the third element is options, the forth is groups (optionally);
  32. if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
  33. var forthArgument = options;
  34. options = groups;
  35. groups = forthArgument;
  36. }
  37. // TODO: REMOVE THIS in the next MAJOR release
  38. // see https://github.com/almende/vis/issues/2511
  39. if (options && options.throttleRedraw) {
  40. console.warn("Timeline option \"throttleRedraw\" is DEPRICATED and no longer supported. It will be removed in the next MAJOR release.");
  41. }
  42. var me = this;
  43. this.defaultOptions = {
  44. start: null,
  45. end: null,
  46. autoResize: true,
  47. orientation: {
  48. axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both'
  49. item: 'bottom' // not relevant
  50. },
  51. moment: moment,
  52. width: null,
  53. height: null,
  54. maxHeight: null,
  55. minHeight: null
  56. };
  57. this.options = util.deepExtend({}, this.defaultOptions);
  58. // Create the DOM, props, and emitter
  59. this._create(container);
  60. if (!options || (options && typeof options.rtl == "undefined")) {
  61. var directionFromDom, domNode = this.dom.root;
  62. while (!directionFromDom && domNode) {
  63. directionFromDom = window.getComputedStyle(domNode, null).direction;
  64. domNode = domNode.parentElement;
  65. }
  66. this.options.rtl = (directionFromDom && (directionFromDom.toLowerCase() == "rtl"));
  67. } else {
  68. this.options.rtl = options.rtl;
  69. }
  70. this.options.rollingMode = options && options.rollingMode;
  71. // all components listed here will be repainted automatically
  72. this.components = [];
  73. this.body = {
  74. dom: this.dom,
  75. domProps: this.props,
  76. emitter: {
  77. on: this.on.bind(this),
  78. off: this.off.bind(this),
  79. emit: this.emit.bind(this)
  80. },
  81. hiddenDates: [],
  82. util: {
  83. getScale: function () {
  84. return me.timeAxis.step.scale;
  85. },
  86. getStep: function () {
  87. return me.timeAxis.step.step;
  88. },
  89. toScreen: me._toScreen.bind(me),
  90. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  91. toTime: me._toTime.bind(me),
  92. toGlobalTime : me._toGlobalTime.bind(me)
  93. }
  94. };
  95. // range
  96. this.range = new Range(this.body, this.options);
  97. this.components.push(this.range);
  98. this.body.range = this.range;
  99. // time axis
  100. this.timeAxis = new TimeAxis(this.body, this.options);
  101. this.timeAxis2 = null; // used in case of orientation option 'both'
  102. this.components.push(this.timeAxis);
  103. // current time bar
  104. this.currentTime = new CurrentTime(this.body, this.options);
  105. this.components.push(this.currentTime);
  106. // item set
  107. this.itemSet = new ItemSet(this.body, this.options);
  108. this.components.push(this.itemSet);
  109. this.itemsData = null; // DataSet
  110. this.groupsData = null; // DataSet
  111. this.dom.root.onclick = function (event) {
  112. me.emit('click', me.getEventProperties(event))
  113. };
  114. this.dom.root.ondblclick = function (event) {
  115. me.emit('doubleClick', me.getEventProperties(event))
  116. };
  117. this.dom.root.oncontextmenu = function (event) {
  118. me.emit('contextmenu', me.getEventProperties(event))
  119. };
  120. this.dom.root.onmouseover = function (event) {
  121. me.emit('mouseOver', me.getEventProperties(event))
  122. };
  123. if(window.PointerEvent) {
  124. this.dom.root.onpointerdown = function (event) {
  125. me.emit('mouseDown', me.getEventProperties(event))
  126. };
  127. this.dom.root.onpointermove = function (event) {
  128. me.emit('mouseMove', me.getEventProperties(event))
  129. };
  130. this.dom.root.onpointerup = function (event) {
  131. me.emit('mouseUp', me.getEventProperties(event))
  132. };
  133. } else {
  134. this.dom.root.onmousemove = function (event) {
  135. me.emit('mouseMove', me.getEventProperties(event))
  136. };
  137. this.dom.root.onmousedown = function (event) {
  138. me.emit('mouseDown', me.getEventProperties(event))
  139. };
  140. this.dom.root.onmouseup = function (event) {
  141. me.emit('mouseUp', me.getEventProperties(event))
  142. };
  143. }
  144. //Single time autoscale/fit
  145. this.fitDone = false;
  146. this.on('changed', function (){
  147. if (this.itemsData == null || this.options.rollingMode) return;
  148. if (!me.fitDone) {
  149. me.fitDone = true;
  150. if (me.options.start != undefined || me.options.end != undefined) {
  151. if (me.options.start == undefined || me.options.end == undefined) {
  152. var range = me.getItemRange();
  153. }
  154. var start = me.options.start != undefined ? me.options.start : range.min;
  155. var end = me.options.end != undefined ? me.options.end : range.max;
  156. me.setWindow(start, end, {animation: false});
  157. }
  158. else {
  159. me.fit({animation: false});
  160. }
  161. }
  162. });
  163. // apply options
  164. if (options) {
  165. this.setOptions(options);
  166. }
  167. // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
  168. if (groups) {
  169. this.setGroups(groups);
  170. }
  171. // create itemset
  172. if (items) {
  173. this.setItems(items);
  174. }
  175. // draw for the first time
  176. this._redraw();
  177. }
  178. // Extend the functionality from Core
  179. Timeline.prototype = new Core();
  180. /**
  181. * Load a configurator
  182. * @return {Object}
  183. * @private
  184. */
  185. Timeline.prototype._createConfigurator = function () {
  186. return new Configurator(this, this.dom.container, configureOptions);
  187. };
  188. /**
  189. * Force a redraw. The size of all items will be recalculated.
  190. * Can be useful to manually redraw when option autoResize=false and the window
  191. * has been resized, or when the items CSS has been changed.
  192. *
  193. * Note: this function will be overridden on construction with a trottled version
  194. */
  195. Timeline.prototype.redraw = function() {
  196. this.itemSet && this.itemSet.markDirty({refreshItems: true});
  197. this._redraw();
  198. };
  199. Timeline.prototype.setOptions = function (options) {
  200. // validate options
  201. let errorFound = Validator.validate(options, allOptions);
  202. if (errorFound === true) {
  203. console.log('%cErrors have been found in the supplied options object.', printStyle);
  204. }
  205. Core.prototype.setOptions.call(this, options);
  206. if ('type' in options) {
  207. if (options.type !== this.options.type) {
  208. this.options.type = options.type;
  209. // force recreation of all items
  210. var itemsData = this.itemsData;
  211. if (itemsData) {
  212. var selection = this.getSelection();
  213. this.setItems(null); // remove all
  214. this.setItems(itemsData); // add all
  215. this.setSelection(selection); // restore selection
  216. }
  217. }
  218. }
  219. };
  220. /**
  221. * Set items
  222. * @param {vis.DataSet | Array | null} items
  223. */
  224. Timeline.prototype.setItems = function(items) {
  225. // convert to type DataSet when needed
  226. var newDataSet;
  227. if (!items) {
  228. newDataSet = null;
  229. }
  230. else if (items instanceof DataSet || items instanceof DataView) {
  231. newDataSet = items;
  232. }
  233. else {
  234. // turn an array into a dataset
  235. newDataSet = new DataSet(items, {
  236. type: {
  237. start: 'Date',
  238. end: 'Date'
  239. }
  240. });
  241. }
  242. // set items
  243. this.itemsData = newDataSet;
  244. this.itemSet && this.itemSet.setItems(newDataSet);
  245. };
  246. /**
  247. * Set groups
  248. * @param {vis.DataSet | Array} groups
  249. */
  250. Timeline.prototype.setGroups = function(groups) {
  251. // convert to type DataSet when needed
  252. var newDataSet;
  253. if (!groups) {
  254. newDataSet = null;
  255. }
  256. else {
  257. var filter = function(group) {
  258. return group.visible !== false;
  259. }
  260. if (groups instanceof DataSet || groups instanceof DataView) {
  261. newDataSet = new DataView(groups,{filter: filter});
  262. }
  263. else {
  264. // turn an array into a dataset
  265. newDataSet = new DataSet(groups.filter(filter));
  266. }
  267. }
  268. this.groupsData = newDataSet;
  269. this.itemSet.setGroups(newDataSet);
  270. };
  271. /**
  272. * Set both items and groups in one go
  273. * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data
  274. */
  275. Timeline.prototype.setData = function (data) {
  276. if (data && data.groups) {
  277. this.setGroups(data.groups);
  278. }
  279. if (data && data.items) {
  280. this.setItems(data.items);
  281. }
  282. };
  283. /**
  284. * Set selected items by their id. Replaces the current selection
  285. * Unknown id's are silently ignored.
  286. * @param {string[] | string} [ids] An array with zero or more id's of the items to be
  287. * selected. If ids is an empty array, all items will be
  288. * unselected.
  289. * @param {Object} [options] Available options:
  290. * `focus: boolean`
  291. * If true, focus will be set to the selected item(s)
  292. * `animation: boolean | {duration: number, easingFunction: string}`
  293. * If true (default), the range is animated
  294. * smoothly to the new window. An object can be
  295. * provided to specify duration and easing function.
  296. * Default duration is 500 ms, and default easing
  297. * function is 'easeInOutQuad'.
  298. * Only applicable when option focus is true.
  299. */
  300. Timeline.prototype.setSelection = function(ids, options) {
  301. this.itemSet && this.itemSet.setSelection(ids);
  302. if (options && options.focus) {
  303. this.focus(ids, options);
  304. }
  305. };
  306. /**
  307. * Get the selected items by their id
  308. * @return {Array} ids The ids of the selected items
  309. */
  310. Timeline.prototype.getSelection = function() {
  311. return this.itemSet && this.itemSet.getSelection() || [];
  312. };
  313. /**
  314. * Adjust the visible window such that the selected item (or multiple items)
  315. * are centered on screen.
  316. * @param {String | String[]} id An item id or array with item ids
  317. * @param {Object} [options] Available options:
  318. * `animation: boolean | {duration: number, easingFunction: string}`
  319. * If true (default), the range is animated
  320. * smoothly to the new window. An object can be
  321. * provided to specify duration and easing function.
  322. * Default duration is 500 ms, and default easing
  323. * function is 'easeInOutQuad'.
  324. */
  325. Timeline.prototype.focus = function(id, options) {
  326. if (!this.itemsData || id == undefined) return;
  327. var ids = Array.isArray(id) ? id : [id];
  328. // get the specified item(s)
  329. var itemsData = this.itemsData.getDataSet().get(ids, {
  330. type: {
  331. start: 'Date',
  332. end: 'Date'
  333. }
  334. });
  335. // calculate minimum start and maximum end of specified items
  336. var start = null;
  337. var end = null;
  338. itemsData.forEach(function (itemData) {
  339. var s = itemData.start.valueOf();
  340. var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
  341. if (start === null || s < start) {
  342. start = s;
  343. }
  344. if (end === null || e > end) {
  345. end = e;
  346. }
  347. });
  348. if (start !== null && end !== null) {
  349. // calculate the new middle and interval for the window
  350. var middle = (start + end) / 2;
  351. var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
  352. var animation = (options && options.animation !== undefined) ? options.animation : true;
  353. this.range.setRange(middle - interval / 2, middle + interval / 2, { animation: animation });
  354. }
  355. };
  356. /**
  357. * Set Timeline window such that it fits all items
  358. * @param {Object} [options] Available options:
  359. * `animation: boolean | {duration: number, easingFunction: string}`
  360. * If true (default), the range is animated
  361. * smoothly to the new window. An object can be
  362. * provided to specify duration and easing function.
  363. * Default duration is 500 ms, and default easing
  364. * function is 'easeInOutQuad'.
  365. */
  366. Timeline.prototype.fit = function (options) {
  367. var animation = (options && options.animation !== undefined) ? options.animation : true;
  368. var range;
  369. var dataset = this.itemsData && this.itemsData.getDataSet();
  370. if (dataset.length === 1 && dataset.get()[0].end === undefined) {
  371. // a single item -> don't fit, just show a range around the item from -4 to +3 days
  372. range = this.getDataRange();
  373. this.moveTo(range.min.valueOf(), {animation});
  374. }
  375. else {
  376. // exactly fit the items (plus a small margin)
  377. range = this.getItemRange();
  378. this.range.setRange(range.min, range.max, { animation: animation });
  379. }
  380. };
  381. /**
  382. * Determine the range of the items, taking into account their actual width
  383. * and a margin of 10 pixels on both sides.
  384. * @return {{min: Date | null, max: Date | null}}
  385. */
  386. Timeline.prototype.getItemRange = function () {
  387. // get a rough approximation for the range based on the items start and end dates
  388. var range = this.getDataRange();
  389. var min = range.min !== null ? range.min.valueOf() : null;
  390. var max = range.max !== null ? range.max.valueOf() : null;
  391. var minItem = null;
  392. var maxItem = null;
  393. if (min != null && max != null) {
  394. var interval = (max - min); // ms
  395. if (interval <= 0) {
  396. interval = 10;
  397. }
  398. var factor = interval / this.props.center.width;
  399. function getStart(item) {
  400. return util.convert(item.data.start, 'Date').valueOf()
  401. }
  402. function getEnd(item) {
  403. var end = item.data.end != undefined ? item.data.end : item.data.start;
  404. return util.convert(end, 'Date').valueOf();
  405. }
  406. // calculate the date of the left side and right side of the items given
  407. util.forEach(this.itemSet.items, function (item) {
  408. if (item.groupShowing) {
  409. item.show();
  410. item.repositionX();
  411. }
  412. var start = getStart(item);
  413. var end = getEnd(item);
  414. if (this.options.rtl) {
  415. var startSide = start - (item.getWidthRight() + 10) * factor;
  416. var endSide = end + (item.getWidthLeft() + 10) * factor;
  417. } else {
  418. var startSide = start - (item.getWidthLeft() + 10) * factor;
  419. var endSide = end + (item.getWidthRight() + 10) * factor;
  420. }
  421. if (startSide < min) {
  422. min = startSide;
  423. minItem = item;
  424. }
  425. if (endSide > max) {
  426. max = endSide;
  427. maxItem = item;
  428. }
  429. }.bind(this));
  430. if (minItem && maxItem) {
  431. var lhs = minItem.getWidthLeft() + 10;
  432. var rhs = maxItem.getWidthRight() + 10;
  433. var delta = this.props.center.width - lhs - rhs; // px
  434. if (delta > 0) {
  435. if (this.options.rtl) {
  436. min = getStart(minItem) - rhs * interval / delta; // ms
  437. max = getEnd(maxItem) + lhs * interval / delta; // ms
  438. } else {
  439. min = getStart(minItem) - lhs * interval / delta; // ms
  440. max = getEnd(maxItem) + rhs * interval / delta; // ms
  441. }
  442. }
  443. }
  444. }
  445. return {
  446. min: min != null ? new Date(min) : null,
  447. max: max != null ? new Date(max) : null
  448. }
  449. };
  450. /**
  451. * Calculate the data range of the items start and end dates
  452. * @returns {{min: Date | null, max: Date | null}}
  453. */
  454. Timeline.prototype.getDataRange = function() {
  455. var min = null;
  456. var max = null;
  457. var dataset = this.itemsData && this.itemsData.getDataSet();
  458. if (dataset) {
  459. dataset.forEach(function (item) {
  460. var start = util.convert(item.start, 'Date').valueOf();
  461. var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf();
  462. if (min === null || start < min) {
  463. min = start;
  464. }
  465. if (max === null || end > max) {
  466. max = end;
  467. }
  468. });
  469. }
  470. return {
  471. min: min != null ? new Date(min) : null,
  472. max: max != null ? new Date(max) : null
  473. }
  474. };
  475. /**
  476. * Generate Timeline related information from an event
  477. * @param {Event} event
  478. * @return {Object} An object with related information, like on which area
  479. * The event happened, whether clicked on an item, etc.
  480. */
  481. Timeline.prototype.getEventProperties = function (event) {
  482. var clientX = event.center ? event.center.x : event.clientX;
  483. var clientY = event.center ? event.center.y : event.clientY;
  484. if (this.options.rtl) {
  485. var x = util.getAbsoluteRight(this.dom.centerContainer) - clientX;
  486. } else {
  487. var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer);
  488. }
  489. var y = clientY - util.getAbsoluteTop(this.dom.centerContainer);
  490. var item = this.itemSet.itemFromTarget(event);
  491. var group = this.itemSet.groupFromTarget(event);
  492. var customTime = CustomTime.customTimeFromTarget(event);
  493. var snap = this.itemSet.options.snap || null;
  494. var scale = this.body.util.getScale();
  495. var step = this.body.util.getStep();
  496. var time = this._toTime(x);
  497. var snappedTime = snap ? snap(time, scale, step) : time;
  498. var element = util.getTarget(event);
  499. var what = null;
  500. if (item != null) {what = 'item';}
  501. else if (customTime != null) {what = 'custom-time';}
  502. else if (util.hasParent(element, this.timeAxis.dom.foreground)) {what = 'axis';}
  503. else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) {what = 'axis';}
  504. else if (util.hasParent(element, this.itemSet.dom.labelSet)) {what = 'group-label';}
  505. else if (util.hasParent(element, this.currentTime.bar)) {what = 'current-time';}
  506. else if (util.hasParent(element, this.dom.center)) {what = 'background';}
  507. return {
  508. event: event,
  509. item: item ? item.id : null,
  510. group: group ? group.groupId : null,
  511. what: what,
  512. pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX,
  513. pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY,
  514. x: x,
  515. y: y,
  516. time: time,
  517. snappedTime: snappedTime
  518. }
  519. };
  520. /**
  521. * Toggle Timeline rolling mode
  522. */
  523. Timeline.prototype.toggleRollingMode = function () {
  524. if (this.range.rolling) {
  525. this.range.stopRolling();
  526. } else {
  527. if (this.options.rollingMode == undefined) {
  528. this.setOptions(this.options)
  529. }
  530. this.range.startRolling();
  531. }
  532. }
  533. module.exports = Timeline;