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.

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