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.

527 lines
16 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
  1. /**
  2. * A horizontal time axis
  3. * @param {Component} parent
  4. * @param {Component[]} [depends] Components on which this components depends
  5. * (except for the parent)
  6. * @param {Object} [options] See TimeAxis.setOptions for the available
  7. * options.
  8. * @constructor TimeAxis
  9. * @extends Component
  10. */
  11. function TimeAxis (parent, depends, options) {
  12. this.id = util.randomUUID();
  13. this.parent = parent;
  14. this.depends = depends;
  15. this.dom = {
  16. majorLines: [],
  17. majorTexts: [],
  18. minorLines: [],
  19. minorTexts: [],
  20. redundant: {
  21. majorLines: [],
  22. majorTexts: [],
  23. minorLines: [],
  24. minorTexts: []
  25. }
  26. };
  27. this.props = {
  28. range: {
  29. start: 0,
  30. end: 0,
  31. minimumStep: 0
  32. },
  33. lineTop: 0
  34. };
  35. this.options = Object.create(parent && parent.options || null);
  36. this.defaultOptions = {
  37. orientation: 'bottom', // supported: 'top', 'bottom'
  38. // TODO: implement timeaxis orientations 'left' and 'right'
  39. showMinorLabels: true,
  40. showMajorLabels: true
  41. };
  42. this.conversion = null;
  43. this.range = null;
  44. this.setOptions(options);
  45. }
  46. TimeAxis.prototype = new Component();
  47. // TODO: comment options
  48. TimeAxis.prototype.setOptions = function (options) {
  49. if (options) {
  50. util.extend(this.options, options);
  51. }
  52. };
  53. /**
  54. * Set a range (start and end)
  55. * @param {Range | Object} range A Range or an object containing start and end.
  56. */
  57. TimeAxis.prototype.setRange = function (range) {
  58. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  59. throw new TypeError('Range must be an instance of Range, ' +
  60. 'or an object containing start and end.');
  61. }
  62. this.range = range;
  63. };
  64. /**
  65. * Convert a position on screen (pixels) to a datetime
  66. * @param {int} x Position on the screen in pixels
  67. * @return {Date} time The datetime the corresponds with given position x
  68. */
  69. TimeAxis.prototype.toTime = function(x) {
  70. var conversion = this.conversion;
  71. return new Date(x / conversion.factor + conversion.offset);
  72. };
  73. /**
  74. * Convert a datetime (Date object) into a position on the screen
  75. * @param {Date} time A date
  76. * @return {int} x The position on the screen in pixels which corresponds
  77. * with the given date.
  78. * @private
  79. */
  80. TimeAxis.prototype.toScreen = function(time) {
  81. var conversion = this.conversion;
  82. return (time.valueOf() - conversion.offset) * conversion.factor;
  83. };
  84. /**
  85. * Repaint the component
  86. * @return {Boolean} changed
  87. */
  88. TimeAxis.prototype.repaint = function () {
  89. var changed = 0,
  90. update = util.updateProperty,
  91. asSize = util.option.asSize,
  92. options = this.options,
  93. orientation = this.getOption('orientation'),
  94. props = this.props,
  95. step = this.step;
  96. var frame = this.frame;
  97. if (!frame) {
  98. frame = document.createElement('div');
  99. this.frame = frame;
  100. changed += 1;
  101. }
  102. frame.className = 'axis ' + orientation;
  103. // TODO: custom className?
  104. if (!frame.parentNode) {
  105. if (!this.parent) {
  106. throw new Error('Cannot repaint time axis: no parent attached');
  107. }
  108. var parentContainer = this.parent.getContainer();
  109. if (!parentContainer) {
  110. throw new Error('Cannot repaint time axis: parent has no container element');
  111. }
  112. parentContainer.appendChild(frame);
  113. changed += 1;
  114. }
  115. var parent = frame.parentNode;
  116. if (parent) {
  117. var beforeChild = frame.nextSibling;
  118. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  119. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  120. (this.props.parentHeight - this.height) + 'px' :
  121. '0px';
  122. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  123. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  124. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  125. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  126. // get characters width and height
  127. this._repaintMeasureChars();
  128. if (this.step) {
  129. this._repaintStart();
  130. step.first();
  131. var xFirstMajorLabel = undefined;
  132. var max = 0;
  133. while (step.hasNext() && max < 1000) {
  134. max++;
  135. var cur = step.getCurrent(),
  136. x = this.toScreen(cur),
  137. isMajor = step.isMajor();
  138. // TODO: lines must have a width, such that we can create css backgrounds
  139. if (this.getOption('showMinorLabels')) {
  140. this._repaintMinorText(x, step.getLabelMinor());
  141. }
  142. if (isMajor && this.getOption('showMajorLabels')) {
  143. if (x > 0) {
  144. if (xFirstMajorLabel == undefined) {
  145. xFirstMajorLabel = x;
  146. }
  147. this._repaintMajorText(x, step.getLabelMajor());
  148. }
  149. this._repaintMajorLine(x);
  150. }
  151. else {
  152. this._repaintMinorLine(x);
  153. }
  154. step.next();
  155. }
  156. // create a major label on the left when needed
  157. if (this.getOption('showMajorLabels')) {
  158. var leftTime = this.toTime(0),
  159. leftText = step.getLabelMajor(leftTime),
  160. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  161. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  162. this._repaintMajorText(0, leftText);
  163. }
  164. }
  165. this._repaintEnd();
  166. }
  167. this._repaintLine();
  168. // put frame online again
  169. if (beforeChild) {
  170. parent.insertBefore(frame, beforeChild);
  171. }
  172. else {
  173. parent.appendChild(frame)
  174. }
  175. }
  176. return (changed > 0);
  177. };
  178. /**
  179. * Start a repaint. Move all DOM elements to a redundant list, where they
  180. * can be picked for re-use, or can be cleaned up in the end
  181. * @private
  182. */
  183. TimeAxis.prototype._repaintStart = function () {
  184. var dom = this.dom,
  185. redundant = dom.redundant;
  186. redundant.majorLines = dom.majorLines;
  187. redundant.majorTexts = dom.majorTexts;
  188. redundant.minorLines = dom.minorLines;
  189. redundant.minorTexts = dom.minorTexts;
  190. dom.majorLines = [];
  191. dom.majorTexts = [];
  192. dom.minorLines = [];
  193. dom.minorTexts = [];
  194. };
  195. /**
  196. * End a repaint. Cleanup leftover DOM elements in the redundant list
  197. * @private
  198. */
  199. TimeAxis.prototype._repaintEnd = function () {
  200. util.forEach(this.dom.redundant, function (arr) {
  201. while (arr.length) {
  202. var elem = arr.pop();
  203. if (elem && elem.parentNode) {
  204. elem.parentNode.removeChild(elem);
  205. }
  206. }
  207. });
  208. };
  209. /**
  210. * Create a minor label for the axis at position x
  211. * @param {Number} x
  212. * @param {String} text
  213. * @private
  214. */
  215. TimeAxis.prototype._repaintMinorText = function (x, text) {
  216. // reuse redundant label
  217. var label = this.dom.redundant.minorTexts.shift();
  218. if (!label) {
  219. // create new label
  220. var content = document.createTextNode('');
  221. label = document.createElement('div');
  222. label.appendChild(content);
  223. label.className = 'text minor';
  224. this.frame.appendChild(label);
  225. }
  226. this.dom.minorTexts.push(label);
  227. label.childNodes[0].nodeValue = text;
  228. label.style.left = x + 'px';
  229. label.style.top = this.props.minorLabelTop + 'px';
  230. //label.title = title; // TODO: this is a heavy operation
  231. };
  232. /**
  233. * Create a Major label for the axis at position x
  234. * @param {Number} x
  235. * @param {String} text
  236. * @private
  237. */
  238. TimeAxis.prototype._repaintMajorText = function (x, text) {
  239. // reuse redundant label
  240. var label = this.dom.redundant.majorTexts.shift();
  241. if (!label) {
  242. // create label
  243. var content = document.createTextNode(text);
  244. label = document.createElement('div');
  245. label.className = 'text major';
  246. label.appendChild(content);
  247. this.frame.appendChild(label);
  248. }
  249. this.dom.majorTexts.push(label);
  250. label.childNodes[0].nodeValue = text;
  251. label.style.top = this.props.majorLabelTop + 'px';
  252. label.style.left = x + 'px';
  253. //label.title = title; // TODO: this is a heavy operation
  254. };
  255. /**
  256. * Create a minor line for the axis at position x
  257. * @param {Number} x
  258. * @private
  259. */
  260. TimeAxis.prototype._repaintMinorLine = function (x) {
  261. // reuse redundant line
  262. var line = this.dom.redundant.minorLines.shift();
  263. if (!line) {
  264. // create vertical line
  265. line = document.createElement('div');
  266. line.className = 'grid vertical minor';
  267. this.frame.appendChild(line);
  268. }
  269. this.dom.minorLines.push(line);
  270. var props = this.props;
  271. line.style.top = props.minorLineTop + 'px';
  272. line.style.height = props.minorLineHeight + 'px';
  273. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  274. };
  275. /**
  276. * Create a Major line for the axis at position x
  277. * @param {Number} x
  278. * @private
  279. */
  280. TimeAxis.prototype._repaintMajorLine = function (x) {
  281. // reuse redundant line
  282. var line = this.dom.redundant.majorLines.shift();
  283. if (!line) {
  284. // create vertical line
  285. line = document.createElement('DIV');
  286. line.className = 'grid vertical major';
  287. this.frame.appendChild(line);
  288. }
  289. this.dom.majorLines.push(line);
  290. var props = this.props;
  291. line.style.top = props.majorLineTop + 'px';
  292. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  293. line.style.height = props.majorLineHeight + 'px';
  294. };
  295. /**
  296. * Repaint the horizontal line for the axis
  297. * @private
  298. */
  299. TimeAxis.prototype._repaintLine = function() {
  300. var line = this.dom.line,
  301. frame = this.frame,
  302. options = this.options;
  303. // line before all axis elements
  304. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  305. if (line) {
  306. // put this line at the end of all childs
  307. frame.removeChild(line);
  308. frame.appendChild(line);
  309. }
  310. else {
  311. // create the axis line
  312. line = document.createElement('div');
  313. line.className = 'grid horizontal major';
  314. frame.appendChild(line);
  315. this.dom.line = line;
  316. }
  317. line.style.top = this.props.lineTop + 'px';
  318. }
  319. else {
  320. if (line && axis.parentElement) {
  321. frame.removeChild(axis.line);
  322. delete this.dom.line;
  323. }
  324. }
  325. };
  326. /**
  327. * Create characters used to determine the size of text on the axis
  328. * @private
  329. */
  330. TimeAxis.prototype._repaintMeasureChars = function () {
  331. // calculate the width and height of a single character
  332. // this is used to calculate the step size, and also the positioning of the
  333. // axis
  334. var dom = this.dom,
  335. text;
  336. if (!dom.measureCharMinor) {
  337. text = document.createTextNode('0');
  338. var measureCharMinor = document.createElement('DIV');
  339. measureCharMinor.className = 'text minor measure';
  340. measureCharMinor.appendChild(text);
  341. this.frame.appendChild(measureCharMinor);
  342. dom.measureCharMinor = measureCharMinor;
  343. }
  344. if (!dom.measureCharMajor) {
  345. text = document.createTextNode('0');
  346. var measureCharMajor = document.createElement('DIV');
  347. measureCharMajor.className = 'text major measure';
  348. measureCharMajor.appendChild(text);
  349. this.frame.appendChild(measureCharMajor);
  350. dom.measureCharMajor = measureCharMajor;
  351. }
  352. };
  353. /**
  354. * Reflow the component
  355. * @return {Boolean} resized
  356. */
  357. TimeAxis.prototype.reflow = function () {
  358. var changed = 0,
  359. update = util.updateProperty,
  360. frame = this.frame,
  361. range = this.range;
  362. if (!range) {
  363. throw new Error('Cannot repaint time axis: no range configured');
  364. }
  365. if (frame) {
  366. changed += update(this, 'top', frame.offsetTop);
  367. changed += update(this, 'left', frame.offsetLeft);
  368. // calculate size of a character
  369. var props = this.props,
  370. showMinorLabels = this.getOption('showMinorLabels'),
  371. showMajorLabels = this.getOption('showMajorLabels'),
  372. measureCharMinor = this.dom.measureCharMinor,
  373. measureCharMajor = this.dom.measureCharMajor;
  374. if (measureCharMinor) {
  375. props.minorCharHeight = measureCharMinor.clientHeight;
  376. props.minorCharWidth = measureCharMinor.clientWidth;
  377. }
  378. if (measureCharMajor) {
  379. props.majorCharHeight = measureCharMajor.clientHeight;
  380. props.majorCharWidth = measureCharMajor.clientWidth;
  381. }
  382. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  383. if (parentHeight != props.parentHeight) {
  384. props.parentHeight = parentHeight;
  385. changed += 1;
  386. }
  387. switch (this.getOption('orientation')) {
  388. case 'bottom':
  389. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  390. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  391. props.minorLabelTop = 0;
  392. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  393. props.minorLineTop = -this.top;
  394. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  395. props.minorLineWidth = 1; // TODO: really calculate width
  396. props.majorLineTop = -this.top;
  397. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  398. props.majorLineWidth = 1; // TODO: really calculate width
  399. props.lineTop = 0;
  400. break;
  401. case 'top':
  402. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  403. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  404. props.majorLabelTop = 0;
  405. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  406. props.minorLineTop = props.minorLabelTop;
  407. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  408. props.minorLineWidth = 1; // TODO: really calculate width
  409. props.majorLineTop = 0;
  410. props.majorLineHeight = Math.max(parentHeight - this.top);
  411. props.majorLineWidth = 1; // TODO: really calculate width
  412. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  413. break;
  414. default:
  415. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  416. }
  417. var height = props.minorLabelHeight + props.majorLabelHeight;
  418. changed += update(this, 'width', frame.offsetWidth);
  419. changed += update(this, 'height', height);
  420. // calculate range and step
  421. this._updateConversion();
  422. var start = util.cast(range.start, 'Date'),
  423. end = util.cast(range.end, 'Date'),
  424. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  425. this.step = new TimeStep(start, end, minimumStep);
  426. changed += update(props.range, 'start', start.valueOf());
  427. changed += update(props.range, 'end', end.valueOf());
  428. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  429. }
  430. return (changed > 0);
  431. };
  432. /**
  433. * Calculate the factor and offset to convert a position on screen to the
  434. * corresponding date and vice versa.
  435. * After the method _updateConversion is executed once, the methods toTime
  436. * and toScreen can be used.
  437. * @private
  438. */
  439. TimeAxis.prototype._updateConversion = function() {
  440. var range = this.range;
  441. if (!range) {
  442. throw new Error('No range configured');
  443. }
  444. if (range.conversion) {
  445. this.conversion = range.conversion(this.width);
  446. }
  447. else {
  448. this.conversion = Range.conversion(range.start, range.end, this.width);
  449. }
  450. };