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.

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