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.

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