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.

636 lines
19 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. var util = require('../../util');
  2. var DOMutil = require('../../DOMutil');
  3. var Component = require('./Component');
  4. var DataStep = require('../DataStep');
  5. /**
  6. * A horizontal time axis
  7. * @param {Object} [options] See DataAxis.setOptions for the available
  8. * options.
  9. * @constructor DataAxis
  10. * @extends Component
  11. * @param body
  12. */
  13. function DataAxis (body, options, svg, linegraphOptions) {
  14. this.id = util.randomUUID();
  15. this.body = body;
  16. this.defaultOptions = {
  17. orientation: 'left', // supported: 'left', 'right'
  18. showMinorLabels: true,
  19. showMajorLabels: true,
  20. showMinorLines: true,
  21. showMajorLines: true,
  22. icons: true,
  23. majorLinesOffset: 7,
  24. minorLinesOffset: 4,
  25. labelOffsetX: 10,
  26. labelOffsetY: 2,
  27. iconWidth: 20,
  28. width: '40px',
  29. visible: true,
  30. alignZeros: true,
  31. customRange: {
  32. left: {min:undefined, max:undefined},
  33. right: {min:undefined, max:undefined}
  34. },
  35. title: {
  36. left: {text:undefined},
  37. right: {text:undefined}
  38. },
  39. format: {
  40. left: {decimals: undefined},
  41. right: {decimals: undefined}
  42. }
  43. };
  44. this.linegraphOptions = linegraphOptions;
  45. this.linegraphSVG = svg;
  46. this.props = {};
  47. this.DOMelements = { // dynamic elements
  48. lines: {},
  49. labels: {},
  50. title: {}
  51. };
  52. this.dom = {};
  53. this.range = {start:0, end:0};
  54. this.options = util.extend({}, this.defaultOptions);
  55. this.conversionFactor = 1;
  56. this.setOptions(options);
  57. this.width = Number(('' + this.options.width).replace("px",""));
  58. this.minWidth = this.width;
  59. this.height = this.linegraphSVG.offsetHeight;
  60. this.hidden = false;
  61. this.stepPixels = 25;
  62. this.stepPixelsForced = 25;
  63. this.zeroCrossing = -1;
  64. this.lineOffset = 0;
  65. this.master = true;
  66. this.svgElements = {};
  67. this.iconsRemoved = false;
  68. this.groups = {};
  69. this.amountOfGroups = 0;
  70. // create the HTML DOM
  71. this._create();
  72. var me = this;
  73. this.body.emitter.on("verticalDrag", function() {
  74. me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px';
  75. });
  76. }
  77. DataAxis.prototype = new Component();
  78. DataAxis.prototype.addGroup = function(label, graphOptions) {
  79. if (!this.groups.hasOwnProperty(label)) {
  80. this.groups[label] = graphOptions;
  81. }
  82. this.amountOfGroups += 1;
  83. };
  84. DataAxis.prototype.updateGroup = function(label, graphOptions) {
  85. this.groups[label] = graphOptions;
  86. };
  87. DataAxis.prototype.removeGroup = function(label) {
  88. if (this.groups.hasOwnProperty(label)) {
  89. delete this.groups[label];
  90. this.amountOfGroups -= 1;
  91. }
  92. };
  93. DataAxis.prototype.setOptions = function (options) {
  94. if (options) {
  95. var redraw = false;
  96. if (this.options.orientation != options.orientation && options.orientation !== undefined) {
  97. redraw = true;
  98. }
  99. var fields = [
  100. 'orientation',
  101. 'showMinorLabels',
  102. 'showMajorLabels',
  103. 'showMajorLines',
  104. 'showMinorLines',
  105. 'icons',
  106. 'majorLinesOffset',
  107. 'minorLinesOffset',
  108. 'labelOffsetX',
  109. 'labelOffsetY',
  110. 'iconWidth',
  111. 'width',
  112. 'visible',
  113. 'customRange',
  114. 'title',
  115. 'format',
  116. 'alignZeros'
  117. ];
  118. util.selectiveExtend(fields, this.options, options);
  119. this.minWidth = Number(('' + this.options.width).replace("px",""));
  120. if (redraw == true && this.dom.frame) {
  121. this.hide();
  122. this.show();
  123. }
  124. }
  125. };
  126. /**
  127. * Create the HTML DOM for the DataAxis
  128. */
  129. DataAxis.prototype._create = function() {
  130. this.dom.frame = document.createElement('div');
  131. this.dom.frame.style.width = this.options.width;
  132. this.dom.frame.style.height = this.height;
  133. this.dom.lineContainer = document.createElement('div');
  134. this.dom.lineContainer.style.width = '100%';
  135. this.dom.lineContainer.style.height = this.height;
  136. this.dom.lineContainer.style.position = 'relative';
  137. // create svg element for graph drawing.
  138. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  139. this.svg.style.position = "absolute";
  140. this.svg.style.top = '0px';
  141. this.svg.style.height = '100%';
  142. this.svg.style.width = '100%';
  143. this.svg.style.display = "block";
  144. this.dom.frame.appendChild(this.svg);
  145. };
  146. DataAxis.prototype._redrawGroupIcons = function () {
  147. DOMutil.prepareElements(this.svgElements);
  148. var x;
  149. var iconWidth = this.options.iconWidth;
  150. var iconHeight = 15;
  151. var iconOffset = 4;
  152. var y = iconOffset + 0.5 * iconHeight;
  153. if (this.options.orientation == 'left') {
  154. x = iconOffset;
  155. }
  156. else {
  157. x = this.width - iconWidth - iconOffset;
  158. }
  159. for (var groupId in this.groups) {
  160. if (this.groups.hasOwnProperty(groupId)) {
  161. if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
  162. this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
  163. y += iconHeight + iconOffset;
  164. }
  165. }
  166. }
  167. DOMutil.cleanupElements(this.svgElements);
  168. this.iconsRemoved = false;
  169. };
  170. DataAxis.prototype._cleanupIcons = function() {
  171. if (this.iconsRemoved == false) {
  172. DOMutil.prepareElements(this.svgElements);
  173. DOMutil.cleanupElements(this.svgElements);
  174. this.iconsRemoved = true;
  175. }
  176. }
  177. /**
  178. * Create the HTML DOM for the DataAxis
  179. */
  180. DataAxis.prototype.show = function() {
  181. this.hidden = false;
  182. if (!this.dom.frame.parentNode) {
  183. if (this.options.orientation == 'left') {
  184. this.body.dom.left.appendChild(this.dom.frame);
  185. }
  186. else {
  187. this.body.dom.right.appendChild(this.dom.frame);
  188. }
  189. }
  190. if (!this.dom.lineContainer.parentNode) {
  191. this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
  192. }
  193. };
  194. /**
  195. * Create the HTML DOM for the DataAxis
  196. */
  197. DataAxis.prototype.hide = function() {
  198. this.hidden = true;
  199. if (this.dom.frame.parentNode) {
  200. this.dom.frame.parentNode.removeChild(this.dom.frame);
  201. }
  202. if (this.dom.lineContainer.parentNode) {
  203. this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
  204. }
  205. };
  206. /**
  207. * Set a range (start and end)
  208. * @param end
  209. * @param start
  210. * @param end
  211. */
  212. DataAxis.prototype.setRange = function (start, end) {
  213. if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) {
  214. if (start > 0) {
  215. start = 0;
  216. }
  217. }
  218. this.range.start = start;
  219. this.range.end = end;
  220. };
  221. /**
  222. * Repaint the component
  223. * @return {boolean} Returns true if the component is resized
  224. */
  225. DataAxis.prototype.redraw = function () {
  226. var changeCalled = false;
  227. var activeGroups = 0;
  228. // Make sure the line container adheres to the vertical scrolling.
  229. this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px';
  230. for (var groupId in this.groups) {
  231. if (this.groups.hasOwnProperty(groupId)) {
  232. if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
  233. activeGroups++;
  234. }
  235. }
  236. }
  237. if (this.amountOfGroups == 0 || activeGroups == 0) {
  238. this.hide();
  239. }
  240. else {
  241. this.show();
  242. this.height = Number(this.linegraphSVG.style.height.replace("px",""));
  243. // svg offsetheight did not work in firefox and explorer...
  244. this.dom.lineContainer.style.height = this.height + 'px';
  245. this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
  246. var props = this.props;
  247. var frame = this.dom.frame;
  248. // update classname
  249. frame.className = 'dataaxis';
  250. // calculate character width and height
  251. this._calculateCharSize();
  252. var orientation = this.options.orientation;
  253. var showMinorLabels = this.options.showMinorLabels;
  254. var showMajorLabels = this.options.showMajorLabels;
  255. // determine the width and height of the elements for the axis
  256. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  257. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  258. props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
  259. props.minorLineHeight = 1;
  260. props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
  261. props.majorLineHeight = 1;
  262. // take frame offline while updating (is almost twice as fast)
  263. if (orientation == 'left') {
  264. frame.style.top = '0';
  265. frame.style.left = '0';
  266. frame.style.bottom = '';
  267. frame.style.width = this.width + 'px';
  268. frame.style.height = this.height + "px";
  269. }
  270. else { // right
  271. frame.style.top = '';
  272. frame.style.bottom = '0';
  273. frame.style.left = '0';
  274. frame.style.width = this.width + 'px';
  275. frame.style.height = this.height + "px";
  276. }
  277. changeCalled = this._redrawLabels();
  278. if (this.options.icons == true) {
  279. this._redrawGroupIcons();
  280. }
  281. else {
  282. this._cleanupIcons();
  283. }
  284. this._redrawTitle(orientation);
  285. }
  286. return changeCalled;
  287. };
  288. /**
  289. * Repaint major and minor text labels and vertical grid lines
  290. * @private
  291. */
  292. DataAxis.prototype._redrawLabels = function () {
  293. DOMutil.prepareElements(this.DOMelements.lines);
  294. DOMutil.prepareElements(this.DOMelements.labels);
  295. var orientation = this.options['orientation'];
  296. // calculate range and step (step such that we have space for 7 characters per label)
  297. var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
  298. var step = new DataStep(
  299. this.range.start,
  300. this.range.end,
  301. minimumStep,
  302. this.dom.frame.offsetHeight,
  303. this.options.customRange[this.options.orientation],
  304. this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on
  305. );
  306. this.step = step;
  307. // get the distance in pixels for a step
  308. // dead space is space that is "left over" after a step
  309. var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step));
  310. this.stepPixels = stepPixels;
  311. var amountOfSteps = this.height / stepPixels;
  312. var stepDifference = 0;
  313. // the slave axis needs to use the same horizontal lines as the master axis.
  314. if (this.master == false) {
  315. stepPixels = this.stepPixelsForced;
  316. stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps);
  317. for (var i = 0; i < 0.5 * stepDifference; i++) {
  318. step.previous();
  319. }
  320. amountOfSteps = this.height / stepPixels;
  321. if (this.zeroCrossing != -1 && this.options.alignZeros == true) {
  322. var zeroStepDifference = (step.marginEnd / step.step) - this.zeroCrossing;
  323. if (zeroStepDifference > 0) {
  324. for (var i = 0; i < zeroStepDifference; i++) {step.next();}
  325. }
  326. else if (zeroStepDifference < 0) {
  327. for (var i = 0; i < -zeroStepDifference; i++) {step.previous();}
  328. }
  329. }
  330. }
  331. else {
  332. amountOfSteps += 0.25;
  333. }
  334. this.valueAtZero = step.marginEnd;
  335. var marginStartPos = 0;
  336. // do not draw the first label
  337. var max = 1;
  338. // Get the number of decimal places
  339. var decimals;
  340. if(this.options.format[orientation] !== undefined) {
  341. decimals = this.options.format[orientation].decimals;
  342. }
  343. this.maxLabelSize = 0;
  344. var y = 0;
  345. while (max < Math.round(amountOfSteps)) {
  346. step.next();
  347. y = Math.round(max * stepPixels);
  348. marginStartPos = max * stepPixels;
  349. var isMajor = step.isMajor();
  350. if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
  351. this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, 'yAxis minor', this.props.minorCharHeight);
  352. }
  353. if (isMajor && this.options['showMajorLabels'] && this.master == true ||
  354. this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
  355. if (y >= 0) {
  356. this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, 'yAxis major', this.props.majorCharHeight);
  357. }
  358. if (this.options.showMajorLines == true) {
  359. this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
  360. }
  361. }
  362. else if (this.options.showMinorLines == true) {
  363. this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
  364. }
  365. if (this.master == true && step.current == 0) {
  366. this.zeroCrossing = max;
  367. }
  368. max++;
  369. }
  370. if (this.master == false) {
  371. this.conversionFactor = y / (this.valueAtZero - step.current);
  372. }
  373. else {
  374. this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange;
  375. }
  376. // Note that title is rotated, so we're using the height, not width!
  377. var titleWidth = 0;
  378. if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) {
  379. titleWidth = this.props.titleCharHeight;
  380. }
  381. var offset = this.options.icons == true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15;
  382. // this will resize the yAxis to accommodate the labels.
  383. if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
  384. this.width = this.maxLabelSize + offset;
  385. this.options.width = this.width + "px";
  386. DOMutil.cleanupElements(this.DOMelements.lines);
  387. DOMutil.cleanupElements(this.DOMelements.labels);
  388. this.redraw();
  389. return true;
  390. }
  391. // this will resize the yAxis if it is too big for the labels.
  392. else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
  393. this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
  394. this.options.width = this.width + "px";
  395. DOMutil.cleanupElements(this.DOMelements.lines);
  396. DOMutil.cleanupElements(this.DOMelements.labels);
  397. this.redraw();
  398. return true;
  399. }
  400. else {
  401. DOMutil.cleanupElements(this.DOMelements.lines);
  402. DOMutil.cleanupElements(this.DOMelements.labels);
  403. return false;
  404. }
  405. };
  406. DataAxis.prototype.convertValue = function (value) {
  407. var invertedValue = this.valueAtZero - value;
  408. var convertedValue = invertedValue * this.conversionFactor;
  409. return convertedValue;
  410. };
  411. /**
  412. * Create a label for the axis at position x
  413. * @private
  414. * @param y
  415. * @param text
  416. * @param orientation
  417. * @param className
  418. * @param characterHeight
  419. */
  420. DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
  421. // reuse redundant label
  422. var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
  423. label.className = className;
  424. label.innerHTML = text;
  425. if (orientation == 'left') {
  426. label.style.left = '-' + this.options.labelOffsetX + 'px';
  427. label.style.textAlign = "right";
  428. }
  429. else {
  430. label.style.right = '-' + this.options.labelOffsetX + 'px';
  431. label.style.textAlign = "left";
  432. }
  433. label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
  434. text += '';
  435. var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
  436. if (this.maxLabelSize < text.length * largestWidth) {
  437. this.maxLabelSize = text.length * largestWidth;
  438. }
  439. };
  440. /**
  441. * Create a minor line for the axis at position y
  442. * @param y
  443. * @param orientation
  444. * @param className
  445. * @param offset
  446. * @param width
  447. */
  448. DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
  449. if (this.master == true) {
  450. var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift();
  451. line.className = className;
  452. line.innerHTML = '';
  453. if (orientation == 'left') {
  454. line.style.left = (this.width - offset) + 'px';
  455. }
  456. else {
  457. line.style.right = (this.width - offset) + 'px';
  458. }
  459. line.style.width = width + 'px';
  460. line.style.top = y + 'px';
  461. }
  462. };
  463. /**
  464. * Create a title for the axis
  465. * @private
  466. * @param orientation
  467. */
  468. DataAxis.prototype._redrawTitle = function (orientation) {
  469. DOMutil.prepareElements(this.DOMelements.title);
  470. // Check if the title is defined for this axes
  471. if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) {
  472. var title = DOMutil.getDOMElement('div', this.DOMelements.title, this.dom.frame);
  473. title.className = 'yAxis title ' + orientation;
  474. title.innerHTML = this.options.title[orientation].text;
  475. // Add style - if provided
  476. if (this.options.title[orientation].style !== undefined) {
  477. util.addCssText(title, this.options.title[orientation].style);
  478. }
  479. if (orientation == 'left') {
  480. title.style.left = this.props.titleCharHeight + 'px';
  481. }
  482. else {
  483. title.style.right = this.props.titleCharHeight + 'px';
  484. }
  485. title.style.width = this.height + 'px';
  486. }
  487. // we need to clean up in case we did not use all elements.
  488. DOMutil.cleanupElements(this.DOMelements.title);
  489. };
  490. /**
  491. * Determine the size of text on the axis (both major and minor axis).
  492. * The size is calculated only once and then cached in this.props.
  493. * @private
  494. */
  495. DataAxis.prototype._calculateCharSize = function () {
  496. // determine the char width and height on the minor axis
  497. if (!('minorCharHeight' in this.props)) {
  498. var textMinor = document.createTextNode('0');
  499. var measureCharMinor = document.createElement('div');
  500. measureCharMinor.className = 'yAxis minor measure';
  501. measureCharMinor.appendChild(textMinor);
  502. this.dom.frame.appendChild(measureCharMinor);
  503. this.props.minorCharHeight = measureCharMinor.clientHeight;
  504. this.props.minorCharWidth = measureCharMinor.clientWidth;
  505. this.dom.frame.removeChild(measureCharMinor);
  506. }
  507. if (!('majorCharHeight' in this.props)) {
  508. var textMajor = document.createTextNode('0');
  509. var measureCharMajor = document.createElement('div');
  510. measureCharMajor.className = 'yAxis major measure';
  511. measureCharMajor.appendChild(textMajor);
  512. this.dom.frame.appendChild(measureCharMajor);
  513. this.props.majorCharHeight = measureCharMajor.clientHeight;
  514. this.props.majorCharWidth = measureCharMajor.clientWidth;
  515. this.dom.frame.removeChild(measureCharMajor);
  516. }
  517. if (!('titleCharHeight' in this.props)) {
  518. var textTitle = document.createTextNode('0');
  519. var measureCharTitle = document.createElement('div');
  520. measureCharTitle.className = 'yAxis title measure';
  521. measureCharTitle.appendChild(textTitle);
  522. this.dom.frame.appendChild(measureCharTitle);
  523. this.props.titleCharHeight = measureCharTitle.clientHeight;
  524. this.props.titleCharWidth = measureCharTitle.clientWidth;
  525. this.dom.frame.removeChild(measureCharTitle);
  526. }
  527. };
  528. /**
  529. * Snap a date to a rounded value.
  530. * The snap intervals are dependent on the current scale and step.
  531. * @param {Date} date the date to be snapped.
  532. * @return {Date} snappedDate
  533. */
  534. DataAxis.prototype.snap = function(date) {
  535. return this.step.snap(date);
  536. };
  537. module.exports = DataAxis;