DataAxis.js 18 KB

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