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.

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