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.

568 lines
18 KiB

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