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.

445 lines
14 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
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. format: TimeStep.FORMAT,
  41. timeAxis: null
  42. };
  43. this.options = util.extend({}, this.defaultOptions);
  44. this.body = body;
  45. // create the HTML DOM
  46. this._create();
  47. this.setOptions(options);
  48. }
  49. TimeAxis.prototype = new Component();
  50. /**
  51. * Set options for the TimeAxis.
  52. * Parameters will be merged in current options.
  53. * @param {Object} options Available options:
  54. * {string} [orientation.axis]
  55. * {boolean} [showMinorLabels]
  56. * {boolean} [showMajorLabels]
  57. */
  58. TimeAxis.prototype.setOptions = function(options) {
  59. if (options) {
  60. // copy all options that we know
  61. util.selectiveExtend([
  62. 'showMinorLabels',
  63. 'showMajorLabels',
  64. 'hiddenDates',
  65. 'timeAxis'
  66. ], this.options, options);
  67. // deep copy the format options
  68. util.selectiveDeepExtend(['format'], this.options, options);
  69. if ('orientation' in options) {
  70. if (typeof options.orientation === 'string') {
  71. this.options.orientation.axis = options.orientation;
  72. }
  73. else if (typeof options.orientation === 'object' && 'axis' in options.orientation) {
  74. this.options.orientation.axis = options.orientation.axis;
  75. }
  76. }
  77. // apply locale to moment.js
  78. // TODO: not so nice, this is applied globally to moment.js
  79. if ('locale' in options) {
  80. if (typeof moment.locale === 'function') {
  81. // moment.js 2.8.1+
  82. moment.locale(options.locale);
  83. }
  84. else {
  85. moment.lang(options.locale);
  86. }
  87. }
  88. }
  89. };
  90. /**
  91. * Create the HTML DOM for the TimeAxis
  92. */
  93. TimeAxis.prototype._create = function() {
  94. this.dom.foreground = document.createElement('div');
  95. this.dom.background = document.createElement('div');
  96. this.dom.foreground.className = 'vis-time-axis vis-foreground';
  97. this.dom.background.className = 'vis-time-axis vis-background';
  98. };
  99. /**
  100. * Destroy the TimeAxis
  101. */
  102. TimeAxis.prototype.destroy = function() {
  103. // remove from DOM
  104. if (this.dom.foreground.parentNode) {
  105. this.dom.foreground.parentNode.removeChild(this.dom.foreground);
  106. }
  107. if (this.dom.background.parentNode) {
  108. this.dom.background.parentNode.removeChild(this.dom.background);
  109. }
  110. this.body = null;
  111. };
  112. /**
  113. * Repaint the component
  114. * @return {boolean} Returns true if the component is resized
  115. */
  116. TimeAxis.prototype.redraw = function () {
  117. var props = this.props;
  118. var foreground = this.dom.foreground;
  119. var background = this.dom.background;
  120. // determine the correct parent DOM element (depending on option orientation)
  121. var parent = (this.options.orientation.axis == 'top') ? this.body.dom.top : this.body.dom.bottom;
  122. var parentChanged = (foreground.parentNode !== parent);
  123. // calculate character width and height
  124. this._calculateCharSize();
  125. // TODO: recalculate sizes only needed when parent is resized or options is changed
  126. var showMinorLabels = this.options.showMinorLabels && this.options.orientation.axis !== 'none';
  127. var showMajorLabels = this.options.showMajorLabels && this.options.orientation.axis !== 'none';
  128. // determine the width and height of the elemens for the axis
  129. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  130. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  131. props.height = props.minorLabelHeight + props.majorLabelHeight;
  132. props.width = foreground.offsetWidth;
  133. props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
  134. (this.options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
  135. props.minorLineWidth = 1; // TODO: really calculate width
  136. props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
  137. props.majorLineWidth = 1; // TODO: really calculate width
  138. // take foreground and background offline while updating (is almost twice as fast)
  139. var foregroundNextSibling = foreground.nextSibling;
  140. var backgroundNextSibling = background.nextSibling;
  141. foreground.parentNode && foreground.parentNode.removeChild(foreground);
  142. background.parentNode && background.parentNode.removeChild(background);
  143. foreground.style.height = this.props.height + 'px';
  144. this._repaintLabels();
  145. // put DOM online again (at the same place)
  146. if (foregroundNextSibling) {
  147. parent.insertBefore(foreground, foregroundNextSibling);
  148. }
  149. else {
  150. parent.appendChild(foreground)
  151. }
  152. if (backgroundNextSibling) {
  153. this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
  154. }
  155. else {
  156. this.body.dom.backgroundVertical.appendChild(background)
  157. }
  158. return this._isResized() || parentChanged;
  159. };
  160. /**
  161. * Repaint major and minor text labels and vertical grid lines
  162. * @private
  163. */
  164. TimeAxis.prototype._repaintLabels = function () {
  165. var orientation = this.options.orientation.axis;
  166. // calculate range and step (step such that we have space for 7 characters per label)
  167. var start = util.convert(this.body.range.start, 'Number');
  168. var end = util.convert(this.body.range.end, 'Number');
  169. var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf();
  170. var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize);
  171. minimumStep -= this.body.util.toTime(0).valueOf();
  172. var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates);
  173. if (this.options.format) {
  174. step.setFormat(this.options.format);
  175. }
  176. if (this.options.timeAxis) {
  177. step.setScale(this.options.timeAxis);
  178. }
  179. this.step = step;
  180. // Move all DOM elements to a "redundant" list, where they
  181. // can be picked for re-use, and clear the lists with lines and texts.
  182. // At the end of the function _repaintLabels, left over elements will be cleaned up
  183. var dom = this.dom;
  184. dom.redundant.lines = dom.lines;
  185. dom.redundant.majorTexts = dom.majorTexts;
  186. dom.redundant.minorTexts = dom.minorTexts;
  187. dom.lines = [];
  188. dom.majorTexts = [];
  189. dom.minorTexts = [];
  190. var cur;
  191. var x = 0;
  192. var isMajor;
  193. var xPrev = 0;
  194. var width = 0;
  195. var prevLine;
  196. var xFirstMajorLabel = undefined;
  197. var max = 0;
  198. var className;
  199. step.first();
  200. while (step.hasNext() && max < 1000) {
  201. max++;
  202. cur = step.getCurrent();
  203. isMajor = step.isMajor();
  204. className = step.getClassName();
  205. xPrev = x;
  206. x = this.body.util.toScreen(cur);
  207. width = x - xPrev;
  208. if (prevLine) {
  209. prevLine.style.width = width + 'px';
  210. }
  211. if (this.options.showMinorLabels) {
  212. this._repaintMinorText(x, step.getLabelMinor(), orientation, className);
  213. }
  214. if (isMajor && this.options.showMajorLabels) {
  215. if (x > 0) {
  216. if (xFirstMajorLabel == undefined) {
  217. xFirstMajorLabel = x;
  218. }
  219. this._repaintMajorText(x, step.getLabelMajor(), orientation, className);
  220. }
  221. prevLine = this._repaintMajorLine(x, orientation, className);
  222. }
  223. else {
  224. prevLine = this._repaintMinorLine(x, orientation, className);
  225. }
  226. step.next();
  227. }
  228. // create a major label on the left when needed
  229. if (this.options.showMajorLabels) {
  230. var leftTime = this.body.util.toTime(0),
  231. leftText = step.getLabelMajor(leftTime),
  232. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  233. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  234. this._repaintMajorText(0, leftText, orientation, className);
  235. }
  236. }
  237. // Cleanup leftover DOM elements from the redundant list
  238. util.forEach(this.dom.redundant, function (arr) {
  239. while (arr.length) {
  240. var elem = arr.pop();
  241. if (elem && elem.parentNode) {
  242. elem.parentNode.removeChild(elem);
  243. }
  244. }
  245. });
  246. };
  247. /**
  248. * Create a minor label for the axis at position x
  249. * @param {Number} x
  250. * @param {String} text
  251. * @param {String} orientation "top" or "bottom" (default)
  252. * @param {String} className
  253. * @private
  254. */
  255. TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) {
  256. // reuse redundant label
  257. var label = this.dom.redundant.minorTexts.shift();
  258. if (!label) {
  259. // create new label
  260. var content = document.createTextNode('');
  261. label = document.createElement('div');
  262. label.appendChild(content);
  263. this.dom.foreground.appendChild(label);
  264. }
  265. this.dom.minorTexts.push(label);
  266. label.childNodes[0].nodeValue = text;
  267. label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
  268. label.style.left = x + 'px';
  269. label.className = 'vis-text vis-minor ' + className;
  270. //label.title = title; // TODO: this is a heavy operation
  271. };
  272. /**
  273. * Create a Major label for the axis at position x
  274. * @param {Number} x
  275. * @param {String} text
  276. * @param {String} orientation "top" or "bottom" (default)
  277. * @param {String} className
  278. * @private
  279. */
  280. TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) {
  281. // reuse redundant label
  282. var label = this.dom.redundant.majorTexts.shift();
  283. if (!label) {
  284. // create label
  285. var content = document.createTextNode(text);
  286. label = document.createElement('div');
  287. label.appendChild(content);
  288. this.dom.foreground.appendChild(label);
  289. }
  290. this.dom.majorTexts.push(label);
  291. label.childNodes[0].nodeValue = text;
  292. label.className = 'vis-text vis-major ' + className;
  293. //label.title = title; // TODO: this is a heavy operation
  294. label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
  295. label.style.left = x + 'px';
  296. };
  297. /**
  298. * Create a minor line for the axis at position x
  299. * @param {Number} x
  300. * @param {String} orientation "top" or "bottom" (default)
  301. * @param {String} className
  302. * @return {Element} Returns the created line
  303. * @private
  304. */
  305. TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) {
  306. // reuse redundant line
  307. var line = this.dom.redundant.lines.shift();
  308. if (!line) {
  309. // create vertical line
  310. line = document.createElement('div');
  311. this.dom.background.appendChild(line);
  312. }
  313. this.dom.lines.push(line);
  314. var props = this.props;
  315. if (orientation == 'top') {
  316. line.style.top = props.majorLabelHeight + 'px';
  317. }
  318. else {
  319. line.style.top = this.body.domProps.top.height + 'px';
  320. }
  321. line.style.height = props.minorLineHeight + 'px';
  322. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  323. line.className = 'vis-grid vis-vertical vis-minor ' + className;
  324. return line;
  325. };
  326. /**
  327. * Create a Major line for the axis at position x
  328. * @param {Number} x
  329. * @param {String} orientation "top" or "bottom" (default)
  330. * @param {String} className
  331. * @return {Element} Returns the created line
  332. * @private
  333. */
  334. TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) {
  335. // reuse redundant line
  336. var line = this.dom.redundant.lines.shift();
  337. if (!line) {
  338. // create vertical line
  339. line = document.createElement('div');
  340. this.dom.background.appendChild(line);
  341. }
  342. this.dom.lines.push(line);
  343. var props = this.props;
  344. if (orientation == 'top') {
  345. line.style.top = '0';
  346. }
  347. else {
  348. line.style.top = this.body.domProps.top.height + 'px';
  349. }
  350. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  351. line.style.height = props.majorLineHeight + 'px';
  352. line.className = 'vis-grid vis-vertical vis-major ' + className;
  353. return line;
  354. };
  355. /**
  356. * Determine the size of text on the axis (both major and minor axis).
  357. * The size is calculated only once and then cached in this.props.
  358. * @private
  359. */
  360. TimeAxis.prototype._calculateCharSize = function () {
  361. // Note: We calculate char size with every redraw. Size may change, for
  362. // example when any of the timelines parents had display:none for example.
  363. // determine the char width and height on the minor axis
  364. if (!this.dom.measureCharMinor) {
  365. this.dom.measureCharMinor = document.createElement('DIV');
  366. this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure';
  367. this.dom.measureCharMinor.style.position = 'absolute';
  368. this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
  369. this.dom.foreground.appendChild(this.dom.measureCharMinor);
  370. }
  371. this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
  372. this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
  373. // determine the char width and height on the major axis
  374. if (!this.dom.measureCharMajor) {
  375. this.dom.measureCharMajor = document.createElement('DIV');
  376. this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure';
  377. this.dom.measureCharMajor.style.position = 'absolute';
  378. this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
  379. this.dom.foreground.appendChild(this.dom.measureCharMajor);
  380. }
  381. this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
  382. this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
  383. };
  384. module.exports = TimeAxis;