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.

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