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.

383 lines
12 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. /**
  13. * Create a timeline visualization
  14. * @param {HTMLElement} container
  15. * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [items]
  16. * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [groups]
  17. * @param {Object} [options] See Timeline.setOptions for the available options.
  18. * @constructor
  19. * @extends Core
  20. */
  21. function Timeline (container, items, groups, options) {
  22. if (!(this instanceof Timeline)) {
  23. throw new SyntaxError('Constructor must be called with the new operator');
  24. }
  25. // if the third element is options, the forth is groups (optionally);
  26. if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
  27. var forthArgument = options;
  28. options = groups;
  29. groups = forthArgument;
  30. }
  31. var me = this;
  32. this.defaultOptions = {
  33. start: null,
  34. end: null,
  35. autoResize: true,
  36. orientation: 'bottom', // 'bottom', 'top', or 'both'
  37. width: null,
  38. height: null,
  39. maxHeight: null,
  40. minHeight: null
  41. };
  42. this.options = util.deepExtend({}, this.defaultOptions);
  43. // Create the DOM, props, and emitter
  44. this._create(container);
  45. // all components listed here will be repainted automatically
  46. this.components = [];
  47. this.body = {
  48. dom: this.dom,
  49. domProps: this.props,
  50. emitter: {
  51. on: this.on.bind(this),
  52. off: this.off.bind(this),
  53. emit: this.emit.bind(this)
  54. },
  55. hiddenDates: [],
  56. util: {
  57. getScale: function () {
  58. return me.timeAxis.step.scale;
  59. },
  60. getStep: function () {
  61. return me.timeAxis.step.step;
  62. },
  63. toScreen: me._toScreen.bind(me),
  64. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  65. toTime: me._toTime.bind(me),
  66. toGlobalTime : me._toGlobalTime.bind(me)
  67. }
  68. };
  69. // range
  70. this.range = new Range(this.body);
  71. this.components.push(this.range);
  72. this.body.range = this.range;
  73. // time axis
  74. this.timeAxis = new TimeAxis(this.body);
  75. this.timeAxis2 = null; // used in case of orientation option 'both'
  76. this.components.push(this.timeAxis);
  77. // current time bar
  78. this.currentTime = new CurrentTime(this.body);
  79. this.components.push(this.currentTime);
  80. // custom time bar
  81. // Note: time bar will be attached in this.setOptions when selected
  82. this.customTime = new CustomTime(this.body);
  83. this.components.push(this.customTime);
  84. // item set
  85. this.itemSet = new ItemSet(this.body);
  86. this.components.push(this.itemSet);
  87. this.itemsData = null; // DataSet
  88. this.groupsData = null; // DataSet
  89. this.on('tap', function (event) {
  90. me.emit('click', me.getEventProperties(event))
  91. });
  92. this.on('doubletap', function (event) {
  93. me.emit('doubleClick', me.getEventProperties(event))
  94. });
  95. this.dom.root.oncontextmenu = function (event) {
  96. me.emit('contextmenu', me.getEventProperties(event))
  97. };
  98. // apply options
  99. if (options) {
  100. this.setOptions(options);
  101. }
  102. // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
  103. if (groups) {
  104. this.setGroups(groups);
  105. }
  106. // create itemset
  107. if (items) {
  108. this.setItems(items);
  109. }
  110. else {
  111. this._redraw();
  112. }
  113. }
  114. // Extend the functionality from Core
  115. Timeline.prototype = new Core();
  116. /**
  117. * Force a redraw. The size of all items will be recalculated.
  118. * Can be useful to manually redraw when option autoResize=false and the window
  119. * has been resized, or when the items CSS has been changed.
  120. */
  121. Timeline.prototype.redraw = function() {
  122. this.itemSet && this.itemSet.markDirty({refreshItems: true});
  123. this._redraw();
  124. };
  125. /**
  126. * Set items
  127. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  128. */
  129. Timeline.prototype.setItems = function(items) {
  130. var initialLoad = (this.itemsData == null);
  131. // convert to type DataSet when needed
  132. var newDataSet;
  133. if (!items) {
  134. newDataSet = null;
  135. }
  136. else if (items instanceof DataSet || items instanceof DataView) {
  137. newDataSet = items;
  138. }
  139. else {
  140. // turn an array into a dataset
  141. newDataSet = new DataSet(items, {
  142. type: {
  143. start: 'Date',
  144. end: 'Date'
  145. }
  146. });
  147. }
  148. // set items
  149. this.itemsData = newDataSet;
  150. this.itemSet && this.itemSet.setItems(newDataSet);
  151. if (initialLoad) {
  152. if (this.options.start != undefined || this.options.end != undefined) {
  153. if (this.options.start == undefined || this.options.end == undefined) {
  154. var dataRange = this._getDataRange();
  155. }
  156. var start = this.options.start != undefined ? this.options.start : dataRange.start;
  157. var end = this.options.end != undefined ? this.options.end : dataRange.end;
  158. this.setWindow(start, end, {animate: false});
  159. }
  160. else {
  161. this.fit({animate: false});
  162. }
  163. }
  164. };
  165. /**
  166. * Set groups
  167. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  168. */
  169. Timeline.prototype.setGroups = function(groups) {
  170. // convert to type DataSet when needed
  171. var newDataSet;
  172. if (!groups) {
  173. newDataSet = null;
  174. }
  175. else if (groups instanceof DataSet || groups instanceof DataView) {
  176. newDataSet = groups;
  177. }
  178. else {
  179. // turn an array into a dataset
  180. newDataSet = new DataSet(groups);
  181. }
  182. this.groupsData = newDataSet;
  183. this.itemSet.setGroups(newDataSet);
  184. };
  185. /**
  186. * Set selected items by their id. Replaces the current selection
  187. * Unknown id's are silently ignored.
  188. * @param {string[] | string} [ids] An array with zero or more id's of the items to be
  189. * selected. If ids is an empty array, all items will be
  190. * unselected.
  191. * @param {Object} [options] Available options:
  192. * `focus: boolean`
  193. * If true, focus will be set to the selected item(s)
  194. * `animate: boolean | number`
  195. * If true (default), the range is animated
  196. * smoothly to the new window.
  197. * If a number, the number is taken as duration
  198. * for the animation. Default duration is 500 ms.
  199. * Only applicable when option focus is true.
  200. */
  201. Timeline.prototype.setSelection = function(ids, options) {
  202. this.itemSet && this.itemSet.setSelection(ids);
  203. if (options && options.focus) {
  204. this.focus(ids, options);
  205. }
  206. };
  207. /**
  208. * Get the selected items by their id
  209. * @return {Array} ids The ids of the selected items
  210. */
  211. Timeline.prototype.getSelection = function() {
  212. return this.itemSet && this.itemSet.getSelection() || [];
  213. };
  214. /**
  215. * Adjust the visible window such that the selected item (or multiple items)
  216. * are centered on screen.
  217. * @param {String | String[]} id An item id or array with item ids
  218. * @param {Object} [options] Available options:
  219. * `animate: boolean | number`
  220. * If true (default), the range is animated
  221. * smoothly to the new window.
  222. * If a number, the number is taken as duration
  223. * for the animation. Default duration is 500 ms.
  224. * Only applicable when option focus is true
  225. */
  226. Timeline.prototype.focus = function(id, options) {
  227. if (!this.itemsData || id == undefined) return;
  228. var ids = Array.isArray(id) ? id : [id];
  229. // get the specified item(s)
  230. var itemsData = this.itemsData.getDataSet().get(ids, {
  231. type: {
  232. start: 'Date',
  233. end: 'Date'
  234. }
  235. });
  236. // calculate minimum start and maximum end of specified items
  237. var start = null;
  238. var end = null;
  239. itemsData.forEach(function (itemData) {
  240. var s = itemData.start.valueOf();
  241. var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
  242. if (start === null || s < start) {
  243. start = s;
  244. }
  245. if (end === null || e > end) {
  246. end = e;
  247. }
  248. });
  249. if (start !== null && end !== null) {
  250. // calculate the new middle and interval for the window
  251. var middle = (start + end) / 2;
  252. var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
  253. var animate = (options && options.animate !== undefined) ? options.animate : true;
  254. this.range.setRange(middle - interval / 2, middle + interval / 2, animate);
  255. }
  256. };
  257. /**
  258. * Get the data range of the item set.
  259. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  260. * When no minimum is found, min==null
  261. * When no maximum is found, max==null
  262. */
  263. Timeline.prototype.getItemRange = function() {
  264. // calculate min from start filed
  265. var dataset = this.itemsData.getDataSet(),
  266. min = null,
  267. max = null;
  268. if (dataset) {
  269. // calculate the minimum value of the field 'start'
  270. var minItem = dataset.min('start');
  271. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  272. // Note: we convert first to Date and then to number because else
  273. // a conversion from ISODate to Number will fail
  274. // calculate maximum value of fields 'start' and 'end'
  275. var maxStartItem = dataset.max('start');
  276. if (maxStartItem) {
  277. max = util.convert(maxStartItem.start, 'Date').valueOf();
  278. }
  279. var maxEndItem = dataset.max('end');
  280. if (maxEndItem) {
  281. if (max == null) {
  282. max = util.convert(maxEndItem.end, 'Date').valueOf();
  283. }
  284. else {
  285. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  286. }
  287. }
  288. }
  289. return {
  290. min: (min != null) ? new Date(min) : null,
  291. max: (max != null) ? new Date(max) : null
  292. };
  293. };
  294. /**
  295. * Generate Timeline related information from an event
  296. * @param {Event} event
  297. * @return {Object} An object with related information, like on which area
  298. * The event happened, whether clicked on an item, etc.
  299. */
  300. Timeline.prototype.getEventProperties = function (event) {
  301. var item = this.itemSet.itemFromTarget(event);
  302. var group = this.itemSet.groupFromTarget(event);
  303. var pageX = event.gesture ? event.gesture.center.pageX : event.pageX;
  304. var pageY = event.gesture ? event.gesture.center.pageY : event.pageY;
  305. var x = pageX - util.getAbsoluteLeft(this.dom.centerContainer);
  306. var y = pageY - util.getAbsoluteTop(this.dom.centerContainer);
  307. var snap = this.itemSet.options.snap || null;
  308. var scale = this.body.util.getScale();
  309. var step = this.body.util.getStep();
  310. var time = this._toTime(x);
  311. var snappedTime = snap ? snap(time, scale, step) : time;
  312. var element = util.getTarget(event);
  313. var what = null;
  314. if (item != null) {what = 'item';}
  315. else if (util.hasParent(element, this.timeAxis.dom.foreground)) {what = 'axis';}
  316. else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) {what = 'axis';}
  317. else if (util.hasParent(element, this.itemSet.dom.labelSet)) {what = 'group-label';}
  318. else if (util.hasParent(element, this.customTime.bar)) {what = 'custom-time';} // TODO: fix for multiple custom time bars
  319. else if (util.hasParent(element, this.currentTime.bar)) {what = 'current-time';}
  320. else if (util.hasParent(element, this.dom.center)) {what = 'background';}
  321. return {
  322. event: event,
  323. item: item ? item.id : null,
  324. group: group ? group.groupId : null,
  325. what: what,
  326. pageX: pageX,
  327. pageY: pageY,
  328. x: x,
  329. y: y,
  330. time: time,
  331. snappedTime: snappedTime
  332. }
  333. };
  334. module.exports = Timeline;