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.

571 lines
18 KiB

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