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.

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