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.

510 lines
16 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  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);
  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;
  197. var next;
  198. var x;
  199. var xNext;
  200. var isMajor, nextIsMajor;
  201. var width = 0, prevWidth;
  202. var line;
  203. var labelMinor;
  204. var xFirstMajorLabel = undefined;
  205. var count = 0;
  206. const MAX = 1000;
  207. var className;
  208. step.start();
  209. next = step.getCurrent();
  210. xNext = this.body.util.toScreen(next);
  211. while (step.hasNext() && count < MAX) {
  212. count++;
  213. isMajor = step.isMajor();
  214. className = step.getClassName();
  215. labelMinor = step.getLabelMinor();
  216. current = next;
  217. x = xNext;
  218. step.next();
  219. next = step.getCurrent();
  220. nextIsMajor = step.isMajor();
  221. xNext = this.body.util.toScreen(next);
  222. prevWidth = width;
  223. width = xNext - x;
  224. var showMinorGrid = (width >= prevWidth * 0.4); // prevent displaying of the 31th of the month on a scale of 5 days
  225. if (this.options.showMinorLabels && showMinorGrid) {
  226. var label = this._repaintMinorText(x, labelMinor, orientation, className);
  227. label.style.width = width + 'px'; // set width to prevent overflow
  228. }
  229. if (isMajor && this.options.showMajorLabels) {
  230. if (x > 0) {
  231. if (xFirstMajorLabel == undefined) {
  232. xFirstMajorLabel = x;
  233. }
  234. label = this._repaintMajorText(x, step.getLabelMajor(), orientation, className);
  235. }
  236. line = this._repaintMajorLine(x, width, orientation, className);
  237. }
  238. else { // minor line
  239. if (showMinorGrid) {
  240. line = this._repaintMinorLine(x, width, orientation, className);
  241. }
  242. else {
  243. if (line) {
  244. // adjust the width of the previous grid
  245. line.style.width = (parseInt (line.style.width) + width) + 'px';
  246. }
  247. }
  248. }
  249. }
  250. if (count === MAX && !warnedForOverflow) {
  251. console.warn(`Something is wrong with the Timeline scale. Limited drawing of grid lines to ${MAX} lines.`);
  252. warnedForOverflow = true;
  253. }
  254. // create a major label on the left when needed
  255. if (this.options.showMajorLabels) {
  256. var leftTime = this.body.util.toTime(0),
  257. leftText = step.getLabelMajor(leftTime),
  258. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  259. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  260. this._repaintMajorText(0, leftText, orientation, className);
  261. }
  262. }
  263. // Cleanup leftover DOM elements from the redundant list
  264. util.forEach(this.dom.redundant, function (arr) {
  265. while (arr.length) {
  266. var elem = arr.pop();
  267. if (elem && elem.parentNode) {
  268. elem.parentNode.removeChild(elem);
  269. }
  270. }
  271. });
  272. };
  273. /**
  274. * Create a minor label for the axis at position x
  275. * @param {Number} x
  276. * @param {String} text
  277. * @param {String} orientation "top" or "bottom" (default)
  278. * @param {String} className
  279. * @return {Element} Returns the HTML element of the created label
  280. * @private
  281. */
  282. TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) {
  283. // reuse redundant label
  284. var label = this.dom.redundant.minorTexts.shift();
  285. if (!label) {
  286. // create new label
  287. var content = document.createTextNode('');
  288. label = document.createElement('div');
  289. label.appendChild(content);
  290. this.dom.foreground.appendChild(label);
  291. }
  292. this.dom.minorTexts.push(label);
  293. label.innerHTML = text;
  294. label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
  295. if (this.options.rtl) {
  296. label.style.left = "";
  297. label.style.right = x + 'px';
  298. } else {
  299. label.style.left = x + 'px';
  300. };
  301. label.className = 'vis-text vis-minor ' + className;
  302. //label.title = title; // TODO: this is a heavy operation
  303. return label;
  304. };
  305. /**
  306. * Create a Major label for the axis at position x
  307. * @param {Number} x
  308. * @param {String} text
  309. * @param {String} orientation "top" or "bottom" (default)
  310. * @param {String} className
  311. * @return {Element} Returns the HTML element of the created label
  312. * @private
  313. */
  314. TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) {
  315. // reuse redundant label
  316. var label = this.dom.redundant.majorTexts.shift();
  317. if (!label) {
  318. // create label
  319. var content = document.createElement('div');
  320. content.innerHTML = text;
  321. label = document.createElement('div');
  322. label.appendChild(content);
  323. this.dom.foreground.appendChild(label);
  324. }
  325. this.dom.majorTexts.push(label);
  326. label.childNodes[0].nodeValue = text;
  327. label.className = 'vis-text vis-major ' + className;
  328. //label.title = title; // TODO: this is a heavy operation
  329. label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
  330. if (this.options.rtl) {
  331. label.style.left = "";
  332. label.style.right = x + 'px';
  333. } else {
  334. label.style.left = x + 'px';
  335. };
  336. return label;
  337. };
  338. /**
  339. * Create a minor line for the axis at position x
  340. * @param {Number} x
  341. * @param {Number} width
  342. * @param {String} orientation "top" or "bottom" (default)
  343. * @param {String} className
  344. * @return {Element} Returns the created line
  345. * @private
  346. */
  347. TimeAxis.prototype._repaintMinorLine = function (x, width, orientation, className) {
  348. // reuse redundant line
  349. var line = this.dom.redundant.lines.shift();
  350. if (!line) {
  351. // create vertical line
  352. line = document.createElement('div');
  353. this.dom.background.appendChild(line);
  354. }
  355. this.dom.lines.push(line);
  356. var props = this.props;
  357. if (orientation == 'top') {
  358. line.style.top = props.majorLabelHeight + 'px';
  359. }
  360. else {
  361. line.style.top = this.body.domProps.top.height + 'px';
  362. }
  363. line.style.height = props.minorLineHeight + 'px';
  364. if (this.options.rtl) {
  365. line.style.left = "";
  366. line.style.right = (x - props.minorLineWidth / 2) + 'px';
  367. line.className = 'vis-grid vis-vertical-rtl vis-minor ' + className;
  368. } else {
  369. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  370. line.className = 'vis-grid vis-vertical vis-minor ' + className;
  371. };
  372. line.style.width = width + 'px';
  373. return line;
  374. };
  375. /**
  376. * Create a Major line for the axis at position x
  377. * @param {Number} x
  378. * @param {Number} width
  379. * @param {String} orientation "top" or "bottom" (default)
  380. * @param {String} className
  381. * @return {Element} Returns the created line
  382. * @private
  383. */
  384. TimeAxis.prototype._repaintMajorLine = function (x, width, orientation, className) {
  385. // reuse redundant line
  386. var line = this.dom.redundant.lines.shift();
  387. if (!line) {
  388. // create vertical line
  389. line = document.createElement('div');
  390. this.dom.background.appendChild(line);
  391. }
  392. this.dom.lines.push(line);
  393. var props = this.props;
  394. if (orientation == 'top') {
  395. line.style.top = '0';
  396. }
  397. else {
  398. line.style.top = this.body.domProps.top.height + 'px';
  399. }
  400. if (this.options.rtl) {
  401. line.style.left = "";
  402. line.style.right = (x - props.majorLineWidth / 2) + 'px';
  403. line.className = 'vis-grid vis-vertical-rtl vis-major ' + className;
  404. } else {
  405. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  406. line.className = 'vis-grid vis-vertical vis-major ' + className;
  407. }
  408. line.style.height = props.majorLineHeight + 'px';
  409. line.style.width = width + 'px';
  410. return line;
  411. };
  412. /**
  413. * Determine the size of text on the axis (both major and minor axis).
  414. * The size is calculated only once and then cached in this.props.
  415. * @private
  416. */
  417. TimeAxis.prototype._calculateCharSize = function () {
  418. // Note: We calculate char size with every redraw. Size may change, for
  419. // example when any of the timelines parents had display:none for example.
  420. // determine the char width and height on the minor axis
  421. if (!this.dom.measureCharMinor) {
  422. this.dom.measureCharMinor = document.createElement('DIV');
  423. this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure';
  424. this.dom.measureCharMinor.style.position = 'absolute';
  425. this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
  426. this.dom.foreground.appendChild(this.dom.measureCharMinor);
  427. }
  428. this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
  429. this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
  430. // determine the char width and height on the major axis
  431. if (!this.dom.measureCharMajor) {
  432. this.dom.measureCharMajor = document.createElement('DIV');
  433. this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure';
  434. this.dom.measureCharMajor.style.position = 'absolute';
  435. this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
  436. this.dom.foreground.appendChild(this.dom.measureCharMajor);
  437. }
  438. this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
  439. this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
  440. };
  441. var warnedForOverflow = false;
  442. module.exports = TimeAxis;