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.

883 lines
29 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 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 TimeAxis = require('./component/TimeAxis');
  8. var CurrentTime = require('./component/CurrentTime');
  9. var CustomTime = require('./component/CustomTime');
  10. var LineGraph = require('./component/LineGraph');
  11. /**
  12. * Create a timeline visualization
  13. * @param {HTMLElement} container
  14. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  15. * @param {Object} [options] See Graph2d.setOptions for the available options.
  16. * @constructor
  17. */
  18. function Graph2d (container, items, options, groups) {
  19. var me = this;
  20. this.defaultOptions = {
  21. start: null,
  22. end: null,
  23. autoResize: true,
  24. orientation: 'bottom',
  25. width: null,
  26. height: null,
  27. maxHeight: null,
  28. minHeight: null
  29. };
  30. this.options = util.deepExtend({}, this.defaultOptions);
  31. // Create the DOM, props, and emitter
  32. this._create(container);
  33. // all components listed here will be repainted automatically
  34. this.components = [];
  35. this.body = {
  36. dom: this.dom,
  37. domProps: this.props,
  38. emitter: {
  39. on: this.on.bind(this),
  40. off: this.off.bind(this),
  41. emit: this.emit.bind(this)
  42. },
  43. util: {
  44. snap: null, // will be specified after TimeAxis is created
  45. toScreen: me._toScreen.bind(me),
  46. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  47. toTime: me._toTime.bind(me),
  48. toGlobalTime : me._toGlobalTime.bind(me)
  49. }
  50. };
  51. // range
  52. this.range = new Range(this.body);
  53. this.components.push(this.range);
  54. this.body.range = this.range;
  55. // time axis
  56. this.timeAxis = new TimeAxis(this.body);
  57. this.components.push(this.timeAxis);
  58. this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
  59. // current time bar
  60. this.currentTime = new CurrentTime(this.body);
  61. this.components.push(this.currentTime);
  62. // custom time bar
  63. // Note: time bar will be attached in this.setOptions when selected
  64. this.customTime = new CustomTime(this.body);
  65. this.components.push(this.customTime);
  66. // item set
  67. this.linegraph = new LineGraph(this.body);
  68. this.components.push(this.linegraph);
  69. this.itemsData = null; // DataSet
  70. this.groupsData = null; // DataSet
  71. // apply options
  72. if (options) {
  73. this.setOptions(options);
  74. }
  75. // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
  76. if (groups) {
  77. this.setGroups(groups);
  78. }
  79. // create itemset
  80. if (items) {
  81. this.setItems(items);
  82. }
  83. else {
  84. this.redraw();
  85. }
  86. }
  87. // turn Graph2d into an event emitter
  88. Emitter(Graph2d.prototype);
  89. /**
  90. * Create the main DOM for the Graph2d: a root panel containing left, right,
  91. * top, bottom, content, and background panel.
  92. * @param {Element} container The container element where the Graph2d will
  93. * be attached.
  94. * @private
  95. */
  96. Graph2d.prototype._create = function (container) {
  97. this.dom = {};
  98. this.dom.root = document.createElement('div');
  99. this.dom.background = document.createElement('div');
  100. this.dom.backgroundVertical = document.createElement('div');
  101. this.dom.backgroundHorizontalContainer = document.createElement('div');
  102. this.dom.centerContainer = document.createElement('div');
  103. this.dom.leftContainer = document.createElement('div');
  104. this.dom.rightContainer = document.createElement('div');
  105. this.dom.backgroundHorizontal = document.createElement('div');
  106. this.dom.center = document.createElement('div');
  107. this.dom.left = document.createElement('div');
  108. this.dom.right = document.createElement('div');
  109. this.dom.top = document.createElement('div');
  110. this.dom.bottom = document.createElement('div');
  111. this.dom.shadowTop = document.createElement('div');
  112. this.dom.shadowBottom = document.createElement('div');
  113. this.dom.shadowTopLeft = document.createElement('div');
  114. this.dom.shadowBottomLeft = document.createElement('div');
  115. this.dom.shadowTopRight = document.createElement('div');
  116. this.dom.shadowBottomRight = document.createElement('div');
  117. this.dom.background.className = 'vispanel background';
  118. this.dom.backgroundVertical.className = 'vispanel background vertical';
  119. this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal';
  120. this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
  121. this.dom.centerContainer.className = 'vispanel center';
  122. this.dom.leftContainer.className = 'vispanel left';
  123. this.dom.rightContainer.className = 'vispanel right';
  124. this.dom.top.className = 'vispanel top';
  125. this.dom.bottom.className = 'vispanel bottom';
  126. this.dom.left.className = 'content';
  127. this.dom.center.className = 'content';
  128. this.dom.right.className = 'content';
  129. this.dom.shadowTop.className = 'shadow top';
  130. this.dom.shadowBottom.className = 'shadow bottom';
  131. this.dom.shadowTopLeft.className = 'shadow top';
  132. this.dom.shadowBottomLeft.className = 'shadow bottom';
  133. this.dom.shadowTopRight.className = 'shadow top';
  134. this.dom.shadowBottomRight.className = 'shadow bottom';
  135. this.dom.root.appendChild(this.dom.background);
  136. this.dom.root.appendChild(this.dom.backgroundVertical);
  137. this.dom.root.appendChild(this.dom.backgroundHorizontalContainer);
  138. this.dom.root.appendChild(this.dom.centerContainer);
  139. this.dom.root.appendChild(this.dom.leftContainer);
  140. this.dom.root.appendChild(this.dom.rightContainer);
  141. this.dom.root.appendChild(this.dom.top);
  142. this.dom.root.appendChild(this.dom.bottom);
  143. this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal);
  144. this.dom.centerContainer.appendChild(this.dom.center);
  145. this.dom.leftContainer.appendChild(this.dom.left);
  146. this.dom.rightContainer.appendChild(this.dom.right);
  147. this.dom.centerContainer.appendChild(this.dom.shadowTop);
  148. this.dom.centerContainer.appendChild(this.dom.shadowBottom);
  149. this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
  150. this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
  151. this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
  152. this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
  153. this.on('rangechange', this.redraw.bind(this));
  154. this.on('change', this.redraw.bind(this));
  155. this.on('touch', this._onTouch.bind(this));
  156. this.on('pinch', this._onPinch.bind(this));
  157. this.on('dragstart', this._onDragStart.bind(this));
  158. this.on('drag', this._onDrag.bind(this));
  159. // create event listeners for all interesting events, these events will be
  160. // emitted via emitter
  161. this.hammer = Hammer(this.dom.root, {
  162. prevent_default: true
  163. });
  164. this.listeners = {};
  165. var me = this;
  166. var events = [
  167. 'touch', 'pinch',
  168. 'tap', 'doubletap', 'hold',
  169. 'dragstart', 'drag', 'dragend',
  170. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
  171. ];
  172. events.forEach(function (event) {
  173. var listener = function () {
  174. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  175. me.emit.apply(me, args);
  176. };
  177. me.hammer.on(event, listener);
  178. me.listeners[event] = listener;
  179. });
  180. // size properties of each of the panels
  181. this.props = {
  182. root: {},
  183. background: {},
  184. centerContainer: {},
  185. leftContainer: {},
  186. rightContainer: {},
  187. center: {},
  188. left: {},
  189. right: {},
  190. top: {},
  191. bottom: {},
  192. border: {},
  193. scrollTop: 0,
  194. scrollTopMin: 0
  195. };
  196. this.touch = {}; // store state information needed for touch events
  197. // attach the root panel to the provided container
  198. if (!container) throw new Error('No container provided');
  199. container.appendChild(this.dom.root);
  200. };
  201. /**
  202. * Destroy the Graph2d, clean up all DOM elements and event listeners.
  203. */
  204. Graph2d.prototype.destroy = function () {
  205. // unbind datasets
  206. this.clear();
  207. // remove all event listeners
  208. this.off();
  209. // stop checking for changed size
  210. this._stopAutoResize();
  211. // remove from DOM
  212. if (this.dom.root.parentNode) {
  213. this.dom.root.parentNode.removeChild(this.dom.root);
  214. }
  215. this.dom = null;
  216. // cleanup hammer touch events
  217. for (var event in this.listeners) {
  218. if (this.listeners.hasOwnProperty(event)) {
  219. delete this.listeners[event];
  220. }
  221. }
  222. this.listeners = null;
  223. this.hammer = null;
  224. // give all components the opportunity to cleanup
  225. this.components.forEach(function (component) {
  226. component.destroy();
  227. });
  228. this.body = null;
  229. };
  230. /**
  231. * Set options. Options will be passed to all components loaded in the Graph2d.
  232. * @param {Object} [options]
  233. * {String} orientation
  234. * Vertical orientation for the Graph2d,
  235. * can be 'bottom' (default) or 'top'.
  236. * {String | Number} width
  237. * Width for the timeline, a number in pixels or
  238. * a css string like '1000px' or '75%'. '100%' by default.
  239. * {String | Number} height
  240. * Fixed height for the Graph2d, a number in pixels or
  241. * a css string like '400px' or '75%'. If undefined,
  242. * The Graph2d will automatically size such that
  243. * its contents fit.
  244. * {String | Number} minHeight
  245. * Minimum height for the Graph2d, a number in pixels or
  246. * a css string like '400px' or '75%'.
  247. * {String | Number} maxHeight
  248. * Maximum height for the Graph2d, a number in pixels or
  249. * a css string like '400px' or '75%'.
  250. * {Number | Date | String} start
  251. * Start date for the visible window
  252. * {Number | Date | String} end
  253. * End date for the visible window
  254. */
  255. Graph2d.prototype.setOptions = function (options) {
  256. if (options) {
  257. // copy the known options
  258. var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
  259. util.selectiveExtend(fields, this.options, options);
  260. // enable/disable autoResize
  261. this._initAutoResize();
  262. }
  263. // propagate options to all components
  264. this.components.forEach(function (component) {
  265. component.setOptions(options);
  266. });
  267. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  268. if (options && options.order) {
  269. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  270. }
  271. // redraw everything
  272. this.redraw();
  273. };
  274. /**
  275. * Set a custom time bar
  276. * @param {Date} time
  277. */
  278. Graph2d.prototype.setCustomTime = function (time) {
  279. if (!this.customTime) {
  280. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  281. }
  282. this.customTime.setCustomTime(time);
  283. };
  284. /**
  285. * Retrieve the current custom time.
  286. * @return {Date} customTime
  287. */
  288. Graph2d.prototype.getCustomTime = function() {
  289. if (!this.customTime) {
  290. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  291. }
  292. return this.customTime.getCustomTime();
  293. };
  294. /**
  295. * Set items
  296. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  297. */
  298. Graph2d.prototype.setItems = function(items) {
  299. var initialLoad = (this.itemsData == null);
  300. // convert to type DataSet when needed
  301. var newDataSet;
  302. if (!items) {
  303. newDataSet = null;
  304. }
  305. else if (items instanceof DataSet || items instanceof DataView) {
  306. newDataSet = items;
  307. }
  308. else {
  309. // turn an array into a dataset
  310. newDataSet = new DataSet(items, {
  311. type: {
  312. start: 'Date',
  313. end: 'Date'
  314. }
  315. });
  316. }
  317. // set items
  318. this.itemsData = newDataSet;
  319. this.linegraph && this.linegraph.setItems(newDataSet);
  320. if (initialLoad && ('start' in this.options || 'end' in this.options)) {
  321. this.fit();
  322. var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
  323. var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
  324. this.setWindow(start, end);
  325. }
  326. };
  327. /**
  328. * Set groups
  329. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  330. */
  331. Graph2d.prototype.setGroups = function(groups) {
  332. // convert to type DataSet when needed
  333. var newDataSet;
  334. if (!groups) {
  335. newDataSet = null;
  336. }
  337. else if (groups instanceof DataSet || groups instanceof DataView) {
  338. newDataSet = groups;
  339. }
  340. else {
  341. // turn an array into a dataset
  342. newDataSet = new DataSet(groups);
  343. }
  344. this.groupsData = newDataSet;
  345. this.linegraph.setGroups(newDataSet);
  346. };
  347. /**
  348. * Clear the Graph2d. By Default, items, groups and options are cleared.
  349. * Example usage:
  350. *
  351. * timeline.clear(); // clear items, groups, and options
  352. * timeline.clear({options: true}); // clear options only
  353. *
  354. * @param {Object} [what] Optionally specify what to clear. By default:
  355. * {items: true, groups: true, options: true}
  356. */
  357. Graph2d.prototype.clear = function(what) {
  358. // clear items
  359. if (!what || what.items) {
  360. this.setItems(null);
  361. }
  362. // clear groups
  363. if (!what || what.groups) {
  364. this.setGroups(null);
  365. }
  366. // clear options of timeline and of each of the components
  367. if (!what || what.options) {
  368. this.components.forEach(function (component) {
  369. component.setOptions(component.defaultOptions);
  370. });
  371. this.setOptions(this.defaultOptions); // this will also do a redraw
  372. }
  373. };
  374. /**
  375. * Set Graph2d window such that it fits all items
  376. */
  377. Graph2d.prototype.fit = function() {
  378. // apply the data range as range
  379. var dataRange = this.getItemRange();
  380. // add 5% space on both sides
  381. var start = dataRange.min;
  382. var end = dataRange.max;
  383. if (start != null && end != null) {
  384. var interval = (end.valueOf() - start.valueOf());
  385. if (interval <= 0) {
  386. // prevent an empty interval
  387. interval = 24 * 60 * 60 * 1000; // 1 day
  388. }
  389. start = new Date(start.valueOf() - interval * 0.05);
  390. end = new Date(end.valueOf() + interval * 0.05);
  391. }
  392. // skip range set if there is no start and end date
  393. if (start === null && end === null) {
  394. return;
  395. }
  396. this.range.setRange(start, end);
  397. };
  398. /**
  399. * Get the data range of the item set.
  400. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  401. * When no minimum is found, min==null
  402. * When no maximum is found, max==null
  403. */
  404. Graph2d.prototype.getItemRange = function() {
  405. // calculate min from start filed
  406. var itemsData = this.itemsData,
  407. min = null,
  408. max = null;
  409. if (itemsData) {
  410. // calculate the minimum value of the field 'start'
  411. var minItem = itemsData.min('start');
  412. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  413. // Note: we convert first to Date and then to number because else
  414. // a conversion from ISODate to Number will fail
  415. // calculate maximum value of fields 'start' and 'end'
  416. var maxStartItem = itemsData.max('start');
  417. if (maxStartItem) {
  418. max = util.convert(maxStartItem.start, 'Date').valueOf();
  419. }
  420. var maxEndItem = itemsData.max('end');
  421. if (maxEndItem) {
  422. if (max == null) {
  423. max = util.convert(maxEndItem.end, 'Date').valueOf();
  424. }
  425. else {
  426. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  427. }
  428. }
  429. }
  430. return {
  431. min: (min != null) ? new Date(min) : null,
  432. max: (max != null) ? new Date(max) : null
  433. };
  434. };
  435. /**
  436. * Set the visible window. Both parameters are optional, you can change only
  437. * start or only end. Syntax:
  438. *
  439. * TimeLine.setWindow(start, end)
  440. * TimeLine.setWindow(range)
  441. *
  442. * Where start and end can be a Date, number, or string, and range is an
  443. * object with properties start and end.
  444. *
  445. * @param {Date | Number | String | Object} [start] Start date of visible window
  446. * @param {Date | Number | String} [end] End date of visible window
  447. */
  448. Graph2d.prototype.setWindow = function(start, end) {
  449. if (arguments.length == 1) {
  450. var range = arguments[0];
  451. this.range.setRange(range.start, range.end);
  452. }
  453. else {
  454. this.range.setRange(start, end);
  455. }
  456. };
  457. /**
  458. * Get the visible window
  459. * @return {{start: Date, end: Date}} Visible range
  460. */
  461. Graph2d.prototype.getWindow = function() {
  462. var range = this.range.getRange();
  463. return {
  464. start: new Date(range.start),
  465. end: new Date(range.end)
  466. };
  467. };
  468. /**
  469. * Force a redraw of the Graph2d. Can be useful to manually redraw when
  470. * option autoResize=false
  471. */
  472. Graph2d.prototype.redraw = function() {
  473. var resized = false,
  474. options = this.options,
  475. props = this.props,
  476. dom = this.dom;
  477. if (!dom) return; // when destroyed
  478. // update class names
  479. dom.root.className = 'vis timeline root ' + options.orientation;
  480. // update root width and height options
  481. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  482. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  483. dom.root.style.width = util.option.asSize(options.width, '');
  484. // calculate border widths
  485. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  486. props.border.right = props.border.left;
  487. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  488. props.border.bottom = props.border.top;
  489. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  490. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  491. // calculate the heights. If any of the side panels is empty, we set the height to
  492. // minus the border width, such that the border will be invisible
  493. props.center.height = dom.center.offsetHeight;
  494. props.left.height = dom.left.offsetHeight;
  495. props.right.height = dom.right.offsetHeight;
  496. props.top.height = dom.top.clientHeight || -props.border.top;
  497. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  498. // TODO: compensate borders when any of the panels is empty.
  499. // apply auto height
  500. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  501. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  502. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  503. borderRootHeight + props.border.top + props.border.bottom;
  504. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  505. // calculate heights of the content panels
  506. props.root.height = dom.root.offsetHeight;
  507. props.background.height = props.root.height - borderRootHeight;
  508. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  509. borderRootHeight;
  510. props.centerContainer.height = containerHeight;
  511. props.leftContainer.height = containerHeight;
  512. props.rightContainer.height = props.leftContainer.height;
  513. // calculate the widths of the panels
  514. props.root.width = dom.root.offsetWidth;
  515. props.background.width = props.root.width - borderRootWidth;
  516. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  517. props.leftContainer.width = props.left.width;
  518. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  519. props.rightContainer.width = props.right.width;
  520. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  521. props.center.width = centerWidth;
  522. props.centerContainer.width = centerWidth;
  523. props.top.width = centerWidth;
  524. props.bottom.width = centerWidth;
  525. // resize the panels
  526. dom.background.style.height = props.background.height + 'px';
  527. dom.backgroundVertical.style.height = props.background.height + 'px';
  528. dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px';
  529. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  530. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  531. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  532. dom.background.style.width = props.background.width + 'px';
  533. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  534. dom.backgroundHorizontalContainer.style.width = props.background.width + 'px';
  535. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  536. dom.centerContainer.style.width = props.center.width + 'px';
  537. dom.top.style.width = props.top.width + 'px';
  538. dom.bottom.style.width = props.bottom.width + 'px';
  539. // reposition the panels
  540. dom.background.style.left = '0';
  541. dom.background.style.top = '0';
  542. dom.backgroundVertical.style.left = props.left.width + 'px';
  543. dom.backgroundVertical.style.top = '0';
  544. dom.backgroundHorizontalContainer.style.left = '0';
  545. dom.backgroundHorizontalContainer.style.top = props.top.height + 'px';
  546. dom.centerContainer.style.left = props.left.width + 'px';
  547. dom.centerContainer.style.top = props.top.height + 'px';
  548. dom.leftContainer.style.left = '0';
  549. dom.leftContainer.style.top = props.top.height + 'px';
  550. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  551. dom.rightContainer.style.top = props.top.height + 'px';
  552. dom.top.style.left = props.left.width + 'px';
  553. dom.top.style.top = '0';
  554. dom.bottom.style.left = props.left.width + 'px';
  555. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  556. // update the scrollTop, feasible range for the offset can be changed
  557. // when the height of the Graph2d or of the contents of the center changed
  558. this._updateScrollTop();
  559. // reposition the scrollable contents
  560. var offset = this.props.scrollTop;
  561. if (options.orientation == 'bottom') {
  562. offset += Math.max(this.props.centerContainer.height - this.props.center.height -
  563. this.props.border.top - this.props.border.bottom, 0);
  564. }
  565. dom.center.style.left = '0';
  566. dom.center.style.top = offset + 'px';
  567. dom.backgroundHorizontal.style.left = '0';
  568. dom.backgroundHorizontal.style.top = offset + 'px';
  569. dom.left.style.left = '0';
  570. dom.left.style.top = offset + 'px';
  571. dom.right.style.left = '0';
  572. dom.right.style.top = offset + 'px';
  573. // show shadows when vertical scrolling is available
  574. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  575. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  576. dom.shadowTop.style.visibility = visibilityTop;
  577. dom.shadowBottom.style.visibility = visibilityBottom;
  578. dom.shadowTopLeft.style.visibility = visibilityTop;
  579. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  580. dom.shadowTopRight.style.visibility = visibilityTop;
  581. dom.shadowBottomRight.style.visibility = visibilityBottom;
  582. // redraw all components
  583. this.components.forEach(function (component) {
  584. resized = component.redraw() || resized;
  585. });
  586. if (resized) {
  587. // keep redrawing until all sizes are settled
  588. this.redraw();
  589. }
  590. };
  591. /**
  592. * Convert a position on screen (pixels) to a datetime
  593. * @param {int} x Position on the screen in pixels
  594. * @return {Date} time The datetime the corresponds with given position x
  595. * @private
  596. */
  597. // TODO: move this function to Range
  598. Graph2d.prototype._toTime = function(x) {
  599. var conversion = this.range.conversion(this.props.center.width);
  600. return new Date(x / conversion.scale + conversion.offset);
  601. };
  602. /**
  603. * Convert a datetime (Date object) into a position on the root
  604. * This is used to get the pixel density estimate for the screen, not the center panel
  605. * @param {Date} time A date
  606. * @return {int} x The position on root in pixels which corresponds
  607. * with the given date.
  608. * @private
  609. */
  610. // TODO: move this function to Range
  611. Graph2d.prototype._toGlobalTime = function(x) {
  612. var conversion = this.range.conversion(this.props.root.width);
  613. return new Date(x / conversion.scale + conversion.offset);
  614. };
  615. /**
  616. * Convert a datetime (Date object) into a position on the screen
  617. * @param {Date} time A date
  618. * @return {int} x The position on the screen in pixels which corresponds
  619. * with the given date.
  620. * @private
  621. */
  622. // TODO: move this function to Range
  623. Graph2d.prototype._toScreen = function(time) {
  624. var conversion = this.range.conversion(this.props.center.width);
  625. return (time.valueOf() - conversion.offset) * conversion.scale;
  626. };
  627. /**
  628. * Convert a datetime (Date object) into a position on the root
  629. * This is used to get the pixel density estimate for the screen, not the center panel
  630. * @param {Date} time A date
  631. * @return {int} x The position on root in pixels which corresponds
  632. * with the given date.
  633. * @private
  634. */
  635. // TODO: move this function to Range
  636. Graph2d.prototype._toGlobalScreen = function(time) {
  637. var conversion = this.range.conversion(this.props.root.width);
  638. return (time.valueOf() - conversion.offset) * conversion.scale;
  639. };
  640. /**
  641. * Initialize watching when option autoResize is true
  642. * @private
  643. */
  644. Graph2d.prototype._initAutoResize = function () {
  645. if (this.options.autoResize == true) {
  646. this._startAutoResize();
  647. }
  648. else {
  649. this._stopAutoResize();
  650. }
  651. };
  652. /**
  653. * Watch for changes in the size of the container. On resize, the Panel will
  654. * automatically redraw itself.
  655. * @private
  656. */
  657. Graph2d.prototype._startAutoResize = function () {
  658. var me = this;
  659. this._stopAutoResize();
  660. this._onResize = function() {
  661. if (me.options.autoResize != true) {
  662. // stop watching when the option autoResize is changed to false
  663. me._stopAutoResize();
  664. return;
  665. }
  666. if (me.dom.root) {
  667. // check whether the frame is resized
  668. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  669. (me.dom.root.clientHeight != me.props.lastHeight)) {
  670. me.props.lastWidth = me.dom.root.clientWidth;
  671. me.props.lastHeight = me.dom.root.clientHeight;
  672. me.emit('change');
  673. }
  674. }
  675. };
  676. // add event listener to window resize
  677. util.addEventListener(window, 'resize', this._onResize);
  678. this.watchTimer = setInterval(this._onResize, 1000);
  679. };
  680. /**
  681. * Stop watching for a resize of the frame.
  682. * @private
  683. */
  684. Graph2d.prototype._stopAutoResize = function () {
  685. if (this.watchTimer) {
  686. clearInterval(this.watchTimer);
  687. this.watchTimer = undefined;
  688. }
  689. // remove event listener on window.resize
  690. util.removeEventListener(window, 'resize', this._onResize);
  691. this._onResize = null;
  692. };
  693. /**
  694. * Start moving the timeline vertically
  695. * @param {Event} event
  696. * @private
  697. */
  698. Graph2d.prototype._onTouch = function (event) {
  699. this.touch.allowDragging = true;
  700. };
  701. /**
  702. * Start moving the timeline vertically
  703. * @param {Event} event
  704. * @private
  705. */
  706. Graph2d.prototype._onPinch = function (event) {
  707. this.touch.allowDragging = false;
  708. };
  709. /**
  710. * Start moving the timeline vertically
  711. * @param {Event} event
  712. * @private
  713. */
  714. Graph2d.prototype._onDragStart = function (event) {
  715. this.touch.initialScrollTop = this.props.scrollTop;
  716. };
  717. /**
  718. * Move the timeline vertically
  719. * @param {Event} event
  720. * @private
  721. */
  722. Graph2d.prototype._onDrag = function (event) {
  723. // refuse to drag when we where pinching to prevent the timeline make a jump
  724. // when releasing the fingers in opposite order from the touch screen
  725. if (!this.touch.allowDragging) return;
  726. var delta = event.gesture.deltaY;
  727. var oldScrollTop = this._getScrollTop();
  728. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  729. if (newScrollTop != oldScrollTop) {
  730. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  731. }
  732. };
  733. /**
  734. * Apply a scrollTop
  735. * @param {Number} scrollTop
  736. * @returns {Number} scrollTop Returns the applied scrollTop
  737. * @private
  738. */
  739. Graph2d.prototype._setScrollTop = function (scrollTop) {
  740. this.props.scrollTop = scrollTop;
  741. this._updateScrollTop();
  742. return this.props.scrollTop;
  743. };
  744. /**
  745. * Update the current scrollTop when the height of the containers has been changed
  746. * @returns {Number} scrollTop Returns the applied scrollTop
  747. * @private
  748. */
  749. Graph2d.prototype._updateScrollTop = function () {
  750. // recalculate the scrollTopMin
  751. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  752. if (scrollTopMin != this.props.scrollTopMin) {
  753. // in case of bottom orientation, change the scrollTop such that the contents
  754. // do not move relative to the time axis at the bottom
  755. if (this.options.orientation == 'bottom') {
  756. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  757. }
  758. this.props.scrollTopMin = scrollTopMin;
  759. }
  760. // limit the scrollTop to the feasible scroll range
  761. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  762. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  763. return this.props.scrollTop;
  764. };
  765. /**
  766. * Get the current scrollTop
  767. * @returns {number} scrollTop
  768. * @private
  769. */
  770. Graph2d.prototype._getScrollTop = function () {
  771. return this.props.scrollTop;
  772. };
  773. module.exports = Graph2d;