TimeAxis.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. var util = require('../../util');
  2. var Component = require('./Component');
  3. var TimeStep = require('../TimeStep');
  4. var DateUtil = require('../DateUtil');
  5. var moment = require('../../module/moment');
  6. /**
  7. * A horizontal time axis
  8. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
  9. * @param {Object} [options] See TimeAxis.setOptions for the available
  10. * options.
  11. * @constructor TimeAxis
  12. * @extends Component
  13. */
  14. function TimeAxis (body, options) {
  15. this.dom = {
  16. foreground: null,
  17. lines: [],
  18. majorTexts: [],
  19. minorTexts: [],
  20. redundant: {
  21. lines: [],
  22. majorTexts: [],
  23. minorTexts: []
  24. }
  25. };
  26. this.props = {
  27. range: {
  28. start: 0,
  29. end: 0,
  30. minimumStep: 0
  31. },
  32. lineTop: 0
  33. };
  34. this.defaultOptions = {
  35. orientation: {
  36. axis: 'bottom'
  37. }, // axis orientation: 'top' or 'bottom'
  38. showMinorLabels: true,
  39. showMajorLabels: true,
  40. maxMinorChars: 7,
  41. format: TimeStep.FORMAT,
  42. moment: moment,
  43. timeAxis: null
  44. };
  45. this.options = util.extend({}, this.defaultOptions);
  46. this.body = body;
  47. // create the HTML DOM
  48. this._create();
  49. this.setOptions(options);
  50. }
  51. TimeAxis.prototype = new Component();
  52. /**
  53. * Set options for the TimeAxis.
  54. * Parameters will be merged in current options.
  55. * @param {Object} options Available options:
  56. * {string} [orientation.axis]
  57. * {boolean} [showMinorLabels]
  58. * {boolean} [showMajorLabels]
  59. */
  60. TimeAxis.prototype.setOptions = function(options) {
  61. if (options) {
  62. // copy all options that we know
  63. util.selectiveExtend([
  64. 'showMinorLabels',
  65. 'showMajorLabels',
  66. 'maxMinorChars',
  67. 'hiddenDates',
  68. 'timeAxis',
  69. 'moment',
  70. 'rtl'
  71. ], this.options, options);
  72. // deep copy the format options
  73. util.selectiveDeepExtend(['format'], this.options, options);
  74. if ('orientation' in options) {
  75. if (typeof options.orientation === 'string') {
  76. this.options.orientation.axis = options.orientation;
  77. }
  78. else if (typeof options.orientation === 'object' && 'axis' in options.orientation) {
  79. this.options.orientation.axis = options.orientation.axis;
  80. }
  81. }
  82. // apply locale to moment.js
  83. // TODO: not so nice, this is applied globally to moment.js
  84. if ('locale' in options) {
  85. if (typeof moment.locale === 'function') {
  86. // moment.js 2.8.1+
  87. moment.locale(options.locale);
  88. }
  89. else {
  90. moment.lang(options.locale);
  91. }
  92. }
  93. }
  94. };
  95. /**
  96. * Create the HTML DOM for the TimeAxis
  97. */
  98. TimeAxis.prototype._create = function() {
  99. this.dom.foreground = document.createElement('div');
  100. this.dom.background = document.createElement('div');
  101. this.dom.foreground.className = 'vis-time-axis vis-foreground';
  102. this.dom.background.className = 'vis-time-axis vis-background';
  103. };
  104. /**
  105. * Destroy the TimeAxis
  106. */
  107. TimeAxis.prototype.destroy = function() {
  108. // remove from DOM
  109. if (this.dom.foreground.parentNode) {
  110. this.dom.foreground.parentNode.removeChild(this.dom.foreground);
  111. }
  112. if (this.dom.background.parentNode) {
  113. this.dom.background.parentNode.removeChild(this.dom.background);
  114. }
  115. this.body = null;
  116. };
  117. /**
  118. * Repaint the component
  119. * @return {boolean} Returns true if the component is resized
  120. */
  121. TimeAxis.prototype.redraw = function () {
  122. var props = this.props;
  123. var foreground = this.dom.foreground;
  124. var background = this.dom.background;
  125. // determine the correct parent DOM element (depending on option orientation)
  126. var parent = (this.options.orientation.axis == 'top') ? this.body.dom.top : this.body.dom.bottom;
  127. var parentChanged = (foreground.parentNode !== parent);
  128. // calculate character width and height
  129. this._calculateCharSize();
  130. // TODO: recalculate sizes only needed when parent is resized or options is changed
  131. var showMinorLabels = this.options.showMinorLabels && this.options.orientation.axis !== 'none';
  132. var showMajorLabels = this.options.showMajorLabels && this.options.orientation.axis !== 'none';
  133. // determine the width and height of the elemens for the axis
  134. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  135. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  136. props.height = props.minorLabelHeight + props.majorLabelHeight;
  137. props.width = foreground.offsetWidth;
  138. props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
  139. (this.options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
  140. props.minorLineWidth = 1; // TODO: really calculate width
  141. props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
  142. props.majorLineWidth = 1; // TODO: really calculate width
  143. // take foreground and background offline while updating (is almost twice as fast)
  144. var foregroundNextSibling = foreground.nextSibling;
  145. var backgroundNextSibling = background.nextSibling;
  146. foreground.parentNode && foreground.parentNode.removeChild(foreground);
  147. background.parentNode && background.parentNode.removeChild(background);
  148. foreground.style.height = this.props.height + 'px';
  149. this._repaintLabels();
  150. // put DOM online again (at the same place)
  151. if (foregroundNextSibling) {
  152. parent.insertBefore(foreground, foregroundNextSibling);
  153. }
  154. else {
  155. parent.appendChild(foreground)
  156. }
  157. if (backgroundNextSibling) {
  158. this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
  159. }
  160. else {
  161. this.body.dom.backgroundVertical.appendChild(background)
  162. }
  163. return this._isResized() || parentChanged;
  164. };
  165. /**
  166. * Repaint major and minor text labels and vertical grid lines
  167. * @private
  168. */
  169. TimeAxis.prototype._repaintLabels = function () {
  170. var orientation = this.options.orientation.axis;
  171. // calculate range and step (step such that we have space for 7 characters per label)
  172. var start = util.convert(this.body.range.start, 'Number');
  173. var end = util.convert(this.body.range.end, 'Number');
  174. var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * this.options.maxMinorChars).valueOf();
  175. var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this.body.range, timeLabelsize);
  176. minimumStep -= this.body.util.toTime(0).valueOf();
  177. var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates, this.options);
  178. step.setMoment(this.options.moment);
  179. if (this.options.format) {
  180. step.setFormat(this.options.format);
  181. }
  182. if (this.options.timeAxis) {
  183. step.setScale(this.options.timeAxis);
  184. }
  185. this.step = step;
  186. // Move all DOM elements to a "redundant" list, where they
  187. // can be picked for re-use, and clear the lists with lines and texts.
  188. // At the end of the function _repaintLabels, left over elements will be cleaned up
  189. var dom = this.dom;
  190. dom.redundant.lines = dom.lines;
  191. dom.redundant.majorTexts = dom.majorTexts;
  192. dom.redundant.minorTexts = dom.minorTexts;
  193. dom.lines = [];
  194. dom.majorTexts = [];
  195. dom.minorTexts = [];
  196. var current; // eslint-disable-line no-unused-vars
  197. var next;
  198. var x;
  199. var xNext;
  200. var isMajor;
  201. var nextIsMajor; // eslint-disable-line no-unused-vars
  202. var showMinorGrid;
  203. var width = 0, prevWidth;
  204. var line;
  205. var labelMinor;
  206. var xFirstMajorLabel = undefined;
  207. var count = 0;
  208. const MAX = 1000;
  209. var className;
  210. step.start();
  211. next = step.getCurrent();
  212. xNext = this.body.util.toScreen(next);
  213. while (step.hasNext() && count < MAX) {
  214. count++;
  215. isMajor = step.isMajor();
  216. className = step.getClassName();
  217. labelMinor = step.getLabelMinor();
  218. current = next;
  219. x = xNext;
  220. step.next();
  221. next = step.getCurrent();
  222. nextIsMajor = step.isMajor();
  223. xNext = this.body.util.toScreen(next);
  224. prevWidth = width;
  225. width = xNext - x;
  226. switch (step.scale) {
  227. case 'week': showMinorGrid = true; break;
  228. default: showMinorGrid = (width >= prevWidth * 0.4); break; // prevent displaying of the 31th of the month on a scale of 5 days
  229. }
  230. if (this.options.showMinorLabels && showMinorGrid) {
  231. var label = this._repaintMinorText(x, labelMinor, orientation, className);
  232. label.style.width = width + 'px'; // set width to prevent overflow
  233. }
  234. if (isMajor && this.options.showMajorLabels) {
  235. if (x > 0) {
  236. if (xFirstMajorLabel == undefined) {
  237. xFirstMajorLabel = x;
  238. }
  239. label = this._repaintMajorText(x, step.getLabelMajor(), orientation, className);
  240. }
  241. line = this._repaintMajorLine(x, width, orientation, className);
  242. }
  243. else { // minor line
  244. if (showMinorGrid) {
  245. line = this._repaintMinorLine(x, width, orientation, className);
  246. }
  247. else {
  248. if (line) {
  249. // adjust the width of the previous grid
  250. line.style.width = (parseInt (line.style.width) + width) + 'px';
  251. }
  252. }
  253. }
  254. }
  255. if (count === MAX && !warnedForOverflow) {
  256. console.warn(`Something is wrong with the Timeline scale. Limited drawing of grid lines to ${MAX} lines.`);
  257. warnedForOverflow = true;
  258. }
  259. // create a major label on the left when needed
  260. if (this.options.showMajorLabels) {
  261. var leftTime = this.body.util.toTime(0),
  262. leftText = step.getLabelMajor(leftTime),
  263. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  264. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  265. this._repaintMajorText(0, leftText, orientation, className);
  266. }
  267. }
  268. // Cleanup leftover DOM elements from the redundant list
  269. util.forEach(this.dom.redundant, function (arr) {
  270. while (arr.length) {
  271. var elem = arr.pop();
  272. if (elem && elem.parentNode) {
  273. elem.parentNode.removeChild(elem);
  274. }
  275. }
  276. });
  277. };
  278. /**
  279. * Create a minor label for the axis at position x
  280. * @param {number} x
  281. * @param {string} text
  282. * @param {string} orientation "top" or "bottom" (default)
  283. * @param {string} className
  284. * @return {Element} Returns the HTML element of the created label
  285. * @private
  286. */
  287. TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) {
  288. // reuse redundant label
  289. var label = this.dom.redundant.minorTexts.shift();
  290. if (!label) {
  291. // create new label
  292. var content = document.createTextNode('');
  293. label = document.createElement('div');
  294. label.appendChild(content);
  295. this.dom.foreground.appendChild(label);
  296. }
  297. this.dom.minorTexts.push(label);
  298. label.innerHTML = text;
  299. label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
  300. if (this.options.rtl) {
  301. label.style.left = "";
  302. label.style.right = x + 'px';
  303. } else {
  304. label.style.left = x + 'px';
  305. }
  306. label.className = 'vis-text vis-minor ' + className;
  307. //label.title = title; // TODO: this is a heavy operation
  308. return label;
  309. };
  310. /**
  311. * Create a Major label for the axis at position x
  312. * @param {number} x
  313. * @param {string} text
  314. * @param {string} orientation "top" or "bottom" (default)
  315. * @param {string} className
  316. * @return {Element} Returns the HTML element of the created label
  317. * @private
  318. */
  319. TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) {
  320. // reuse redundant label
  321. var label = this.dom.redundant.majorTexts.shift();
  322. if (!label) {
  323. // create label
  324. var content = document.createElement('div');
  325. label = document.createElement('div');
  326. label.appendChild(content);
  327. this.dom.foreground.appendChild(label);
  328. }
  329. label.childNodes[0].innerHTML = text;
  330. label.className = 'vis-text vis-major ' + className;
  331. //label.title = title; // TODO: this is a heavy operation
  332. label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
  333. if (this.options.rtl) {
  334. label.style.left = "";
  335. label.style.right = x + 'px';
  336. } else {
  337. label.style.left = x + 'px';
  338. }
  339. this.dom.majorTexts.push(label);
  340. return label;
  341. };
  342. /**
  343. * Create a minor line for the axis at position x
  344. * @param {number} x
  345. * @param {number} width
  346. * @param {string} orientation "top" or "bottom" (default)
  347. * @param {string} className
  348. * @return {Element} Returns the created line
  349. * @private
  350. */
  351. TimeAxis.prototype._repaintMinorLine = function (x, width, orientation, className) {
  352. // reuse redundant line
  353. var line = this.dom.redundant.lines.shift();
  354. if (!line) {
  355. // create vertical line
  356. line = document.createElement('div');
  357. this.dom.background.appendChild(line);
  358. }
  359. this.dom.lines.push(line);
  360. var props = this.props;
  361. if (orientation == 'top') {
  362. line.style.top = props.majorLabelHeight + 'px';
  363. }
  364. else {
  365. line.style.top = this.body.domProps.top.height + 'px';
  366. }
  367. line.style.height = props.minorLineHeight + 'px';
  368. if (this.options.rtl) {
  369. line.style.left = "";
  370. line.style.right = (x - props.minorLineWidth / 2) + 'px';
  371. line.className = 'vis-grid vis-vertical-rtl vis-minor ' + className;
  372. } else {
  373. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  374. line.className = 'vis-grid vis-vertical vis-minor ' + className;
  375. }
  376. line.style.width = width + 'px';
  377. return line;
  378. };
  379. /**
  380. * Create a Major line for the axis at position x
  381. * @param {number} x
  382. * @param {number} width
  383. * @param {string} orientation "top" or "bottom" (default)
  384. * @param {string} className
  385. * @return {Element} Returns the created line
  386. * @private
  387. */
  388. TimeAxis.prototype._repaintMajorLine = function (x, width, orientation, className) {
  389. // reuse redundant line
  390. var line = this.dom.redundant.lines.shift();
  391. if (!line) {
  392. // create vertical line
  393. line = document.createElement('div');
  394. this.dom.background.appendChild(line);
  395. }
  396. this.dom.lines.push(line);
  397. var props = this.props;
  398. if (orientation == 'top') {
  399. line.style.top = '0';
  400. }
  401. else {
  402. line.style.top = this.body.domProps.top.height + 'px';
  403. }
  404. if (this.options.rtl) {
  405. line.style.left = "";
  406. line.style.right = (x - props.majorLineWidth / 2) + 'px';
  407. line.className = 'vis-grid vis-vertical-rtl vis-major ' + className;
  408. } else {
  409. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  410. line.className = 'vis-grid vis-vertical vis-major ' + className;
  411. }
  412. line.style.height = props.majorLineHeight + 'px';
  413. line.style.width = width + 'px';
  414. return line;
  415. };
  416. /**
  417. * Determine the size of text on the axis (both major and minor axis).
  418. * The size is calculated only once and then cached in this.props.
  419. * @private
  420. */
  421. TimeAxis.prototype._calculateCharSize = function () {
  422. // Note: We calculate char size with every redraw. Size may change, for
  423. // example when any of the timelines parents had display:none for example.
  424. // determine the char width and height on the minor axis
  425. if (!this.dom.measureCharMinor) {
  426. this.dom.measureCharMinor = document.createElement('DIV');
  427. this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure';
  428. this.dom.measureCharMinor.style.position = 'absolute';
  429. this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
  430. this.dom.foreground.appendChild(this.dom.measureCharMinor);
  431. }
  432. this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
  433. this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
  434. // determine the char width and height on the major axis
  435. if (!this.dom.measureCharMajor) {
  436. this.dom.measureCharMajor = document.createElement('DIV');
  437. this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure';
  438. this.dom.measureCharMajor.style.position = 'absolute';
  439. this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
  440. this.dom.foreground.appendChild(this.dom.measureCharMajor);
  441. }
  442. this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
  443. this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
  444. };
  445. var warnedForOverflow = false;
  446. module.exports = TimeAxis;