Stacking.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. /* *
  2. *
  3. * (c) 2010-2019 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import H from './Globals.js';
  12. /**
  13. * Stack of data points
  14. *
  15. * @product highcharts
  16. *
  17. * @interface Highcharts.StackItemObject
  18. */ /**
  19. * Alignment settings
  20. * @name Highcharts.StackItemObject#alignOptions
  21. * @type {Highcharts.AlignObject}
  22. */ /**
  23. * Related axis
  24. * @name Highcharts.StackItemObject#axis
  25. * @type {Highcharts.Axis}
  26. */ /**
  27. * Cumulative value of the stacked data points
  28. * @name Highcharts.StackItemObject#cumulative
  29. * @type {number}
  30. */ /**
  31. * True if on the negative side
  32. * @name Highcharts.StackItemObject#isNegative
  33. * @type {boolean}
  34. */ /**
  35. * Related SVG element
  36. * @name Highcharts.StackItemObject#label
  37. * @type {Highcharts.SVGElement}
  38. */ /**
  39. * Related stack options
  40. * @name Highcharts.StackItemObject#options
  41. * @type {Highcharts.YAxisStackLabelsOptions}
  42. */ /**
  43. * Total value of the stacked data points
  44. * @name Highcharts.StackItemObject#total
  45. * @type {number}
  46. */ /**
  47. * Shared x value of the stack
  48. * @name Highcharts.StackItemObject#x
  49. * @type {number}
  50. */
  51. import U from './Utilities.js';
  52. var correctFloat = U.correctFloat, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, objectEach = U.objectEach, pick = U.pick;
  53. import './Axis.js';
  54. import './Chart.js';
  55. import './Series.js';
  56. var Axis = H.Axis, Chart = H.Chart, format = H.format, Series = H.Series;
  57. /* eslint-disable no-invalid-this, valid-jsdoc */
  58. /**
  59. * The class for stacks. Each stack, on a specific X value and either negative
  60. * or positive, has its own stack item.
  61. *
  62. * @private
  63. * @class
  64. * @name Highcharts.StackItem
  65. * @param {Highcharts.Axis} axis
  66. * @param {Highcharts.YAxisStackLabelsOptions} options
  67. * @param {boolean} isNegative
  68. * @param {number} x
  69. * @param {Highcharts.OptionsStackingValue} [stackOption]
  70. */
  71. H.StackItem = function (axis, options, isNegative, x, stackOption) {
  72. var inverted = axis.chart.inverted;
  73. this.axis = axis;
  74. // Tells if the stack is negative
  75. this.isNegative = isNegative;
  76. // Save the options to be able to style the label
  77. this.options = options = options || {};
  78. // Save the x value to be able to position the label later
  79. this.x = x;
  80. // Initialize total value
  81. this.total = null;
  82. // This will keep each points' extremes stored by series.index and point
  83. // index
  84. this.points = {};
  85. // Save the stack option on the series configuration object, and whether to
  86. // treat it as percent
  87. this.stack = stackOption;
  88. this.leftCliff = 0;
  89. this.rightCliff = 0;
  90. // The align options and text align varies on whether the stack is negative
  91. // and if the chart is inverted or not.
  92. // First test the user supplied value, then use the dynamic.
  93. this.alignOptions = {
  94. align: options.align ||
  95. (inverted ? (isNegative ? 'left' : 'right') : 'center'),
  96. verticalAlign: options.verticalAlign ||
  97. (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
  98. y: options.y,
  99. x: options.x
  100. };
  101. this.textAlign = options.textAlign ||
  102. (inverted ? (isNegative ? 'right' : 'left') : 'center');
  103. };
  104. H.StackItem.prototype = {
  105. /**
  106. * @private
  107. * @function Highcharts.StackItem#destroy
  108. * @return {void}
  109. */
  110. destroy: function () {
  111. destroyObjectProperties(this, this.axis);
  112. },
  113. /**
  114. * Renders the stack total label and adds it to the stack label group.
  115. *
  116. * @private
  117. * @function Highcharts.StackItem#render
  118. * @param {Highcharts.SVGElement} group
  119. * @return {void}
  120. */
  121. render: function (group) {
  122. var chart = this.axis.chart, options = this.options, formatOption = options.format, attr = {}, str = formatOption ? // format the text in the label
  123. format(formatOption, this, chart) :
  124. options.formatter.call(this);
  125. // Change the text to reflect the new total and set visibility to hidden
  126. // in case the serie is hidden
  127. if (this.label) {
  128. this.label.attr({ text: str, visibility: 'hidden' });
  129. }
  130. else {
  131. // Create new label
  132. this.label = chart.renderer
  133. .label(str, null, null, options.shape, null, null, options.useHTML, false, 'stack-labels');
  134. attr = {
  135. text: str,
  136. align: this.textAlign,
  137. rotation: options.rotation,
  138. padding: pick(options.padding, 0),
  139. visibility: 'hidden' // hidden until setOffset is called
  140. };
  141. this.label.attr(attr);
  142. if (!chart.styledMode) {
  143. this.label.css(options.style);
  144. }
  145. if (!this.label.added) {
  146. this.label.add(group); // add to the labels-group
  147. }
  148. }
  149. // Rank it higher than data labels (#8742)
  150. this.label.labelrank = chart.plotHeight;
  151. },
  152. /**
  153. * Sets the offset that the stack has from the x value and repositions the
  154. * label.
  155. *
  156. * @private
  157. * @function Highcarts.StackItem#setOffset
  158. * @param {number} xOffset
  159. * @param {number} xWidth
  160. * @param {number} [boxBottom]
  161. * @param {number} [boxTop]
  162. * @param {number} [defaultX]
  163. * @return {void}
  164. */
  165. setOffset: function (xOffset, xWidth, boxBottom, boxTop, defaultX) {
  166. var stackItem = this, axis = stackItem.axis, chart = axis.chart,
  167. // stack value translated mapped to chart coordinates
  168. y = axis.translate(axis.usePercentage ?
  169. 100 :
  170. (boxTop ?
  171. boxTop :
  172. stackItem.total), 0, 0, 0, 1), yZero = axis.translate(boxBottom ? boxBottom : 0), // stack origin
  173. // stack height:
  174. h = defined(y) && Math.abs(y - yZero),
  175. // x position:
  176. x = pick(defaultX, chart.xAxis[0].translate(stackItem.x)) +
  177. xOffset, stackBox = defined(y) && stackItem.getStackBox(chart, stackItem, x, y, xWidth, h, axis), label = stackItem.label, isNegative = stackItem.isNegative, isJustify = pick(stackItem.options.overflow, 'justify') === 'justify', visible, alignAttr;
  178. if (label && stackBox) {
  179. var bBox = label.getBBox(), boxOffsetX = chart.inverted ?
  180. (isNegative ? bBox.width : 0) : bBox.width / 2, boxOffsetY = chart.inverted ?
  181. bBox.height / 2 : (isNegative ? -4 : bBox.height + 4);
  182. stackItem.alignOptions.x = pick(stackItem.options.x, 0);
  183. // Align the label to the box
  184. label.align(stackItem.alignOptions, null, stackBox);
  185. // Set visibility (#678)
  186. alignAttr = label.alignAttr;
  187. label.show();
  188. // Set label above/under stackBox
  189. alignAttr.y -= boxOffsetY;
  190. if (isJustify) {
  191. // Set label x position for justifyDataLabel function
  192. alignAttr.x -= boxOffsetX;
  193. Series.prototype.justifyDataLabel.call(this.axis, label, stackItem.alignOptions, alignAttr, bBox, stackBox);
  194. alignAttr.x += boxOffsetX;
  195. }
  196. alignAttr.x = label.alignAttr.x;
  197. label.attr({
  198. x: alignAttr.x,
  199. y: alignAttr.y
  200. });
  201. if (pick(!isJustify && stackItem.options.crop, true)) {
  202. visible = chart.isInsidePlot(label.x +
  203. (chart.inverted ? 0 : -bBox.width / 2), label.y) &&
  204. chart.isInsidePlot(label.x + (chart.inverted ?
  205. (isNegative ? -bBox.width : bBox.width) :
  206. bBox.width / 2), label.y + bBox.height);
  207. if (!visible) {
  208. label.hide();
  209. }
  210. }
  211. }
  212. },
  213. /**
  214. * @private
  215. * @function Highcharts.StackItem#getStackBox
  216. *
  217. * @param {Highcharts.Chart} chart
  218. *
  219. * @param {Highcharts.StackItem} stackItem
  220. *
  221. * @param {number} x
  222. *
  223. * @param {number} y
  224. *
  225. * @param {number} xWidth
  226. *
  227. * @param {number} h
  228. *
  229. * @param {Highcharts.Axis} axis
  230. *
  231. * @return {Highcharts.BBoxObject}
  232. */
  233. getStackBox: function (chart, stackItem, x, y, xWidth, h, axis) {
  234. var reversed = stackItem.axis.reversed, inverted = chart.inverted, axisPos = axis.height + axis.pos -
  235. (inverted ? chart.plotLeft : chart.plotTop), neg = (stackItem.isNegative && !reversed) ||
  236. (!stackItem.isNegative && reversed); // #4056
  237. return {
  238. x: inverted ? (neg ? y : y - h) : x,
  239. y: inverted ?
  240. axisPos - x - xWidth :
  241. (neg ?
  242. (axisPos - y - h) :
  243. axisPos - y),
  244. width: inverted ? h : xWidth,
  245. height: inverted ? xWidth : h
  246. };
  247. }
  248. };
  249. /**
  250. * Generate stacks for each series and calculate stacks total values
  251. *
  252. * @private
  253. * @function Highcharts.Chart#getStacks
  254. * @return {void}
  255. */
  256. Chart.prototype.getStacks = function () {
  257. var chart = this, inverted = chart.inverted;
  258. // reset stacks for each yAxis
  259. chart.yAxis.forEach(function (axis) {
  260. if (axis.stacks && axis.hasVisibleSeries) {
  261. axis.oldStacks = axis.stacks;
  262. }
  263. });
  264. chart.series.forEach(function (series) {
  265. var xAxisOptions = series.xAxis && series.xAxis.options || {};
  266. if (series.options.stacking &&
  267. (series.visible === true ||
  268. chart.options.chart.ignoreHiddenSeries === false)) {
  269. series.stackKey = [
  270. series.type,
  271. pick(series.options.stack, ''),
  272. inverted ? xAxisOptions.top : xAxisOptions.left,
  273. inverted ? xAxisOptions.height : xAxisOptions.width
  274. ].join(',');
  275. }
  276. });
  277. };
  278. // Stacking methods defined on the Axis prototype
  279. /**
  280. * Build the stacks from top down
  281. *
  282. * @private
  283. * @function Highcharts.Axis#buildStacks
  284. * @return {void}
  285. */
  286. Axis.prototype.buildStacks = function () {
  287. var axisSeries = this.series, reversedStacks = pick(this.options.reversedStacks, true), len = axisSeries.length, actualSeries, i;
  288. if (!this.isXAxis) {
  289. this.usePercentage = false;
  290. i = len;
  291. while (i--) {
  292. actualSeries = axisSeries[reversedStacks ? i : len - i - 1];
  293. actualSeries.setStackedPoints();
  294. }
  295. // Loop up again to compute percent and stream stack
  296. for (i = 0; i < len; i++) {
  297. axisSeries[i].modifyStacks();
  298. }
  299. H.fireEvent(this, 'afterBuildStacks');
  300. }
  301. };
  302. /**
  303. * @private
  304. * @function Highcharts.Axis#renderStackTotals
  305. * @return {vopid}
  306. */
  307. Axis.prototype.renderStackTotals = function () {
  308. var axis = this, chart = axis.chart, renderer = chart.renderer, stacks = axis.stacks, stackTotalGroup = axis.stackTotalGroup;
  309. // Create a separate group for the stack total labels
  310. if (!stackTotalGroup) {
  311. axis.stackTotalGroup = stackTotalGroup =
  312. renderer
  313. .g('stack-labels')
  314. .attr({
  315. visibility: 'visible',
  316. zIndex: 6
  317. })
  318. .add();
  319. }
  320. // plotLeft/Top will change when y axis gets wider so we need to translate
  321. // the stackTotalGroup at every render call. See bug #506 and #516
  322. stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
  323. // Render each stack total
  324. objectEach(stacks, function (type) {
  325. objectEach(type, function (stack) {
  326. stack.render(stackTotalGroup);
  327. });
  328. });
  329. };
  330. /**
  331. * Set all the stacks to initial states and destroy unused ones.
  332. *
  333. * @private
  334. * @function Highcharts.Axis#resetStacks
  335. * @return {void}
  336. */
  337. Axis.prototype.resetStacks = function () {
  338. var axis = this, stacks = axis.stacks;
  339. if (!axis.isXAxis) {
  340. objectEach(stacks, function (type) {
  341. objectEach(type, function (stack, key) {
  342. // Clean up memory after point deletion (#1044, #4320)
  343. if (stack.touched < axis.stacksTouched) {
  344. stack.destroy();
  345. delete type[key];
  346. // Reset stacks
  347. }
  348. else {
  349. stack.total = null;
  350. stack.cumulative = null;
  351. }
  352. });
  353. });
  354. }
  355. };
  356. /**
  357. * @private
  358. * @function Highcharts.Axis#cleanStacks
  359. * @return {void}
  360. */
  361. Axis.prototype.cleanStacks = function () {
  362. var stacks;
  363. if (!this.isXAxis) {
  364. if (this.oldStacks) {
  365. stacks = this.stacks = this.oldStacks;
  366. }
  367. // reset stacks
  368. objectEach(stacks, function (type) {
  369. objectEach(type, function (stack) {
  370. stack.cumulative = stack.total;
  371. });
  372. });
  373. }
  374. };
  375. // Stacking methods defnied for Series prototype
  376. /**
  377. * Adds series' points value to corresponding stack
  378. *
  379. * @private
  380. * @function Highcharts.Series#setStackedPoints
  381. * @return {void}
  382. */
  383. Series.prototype.setStackedPoints = function () {
  384. if (!this.options.stacking ||
  385. (this.visible !== true &&
  386. this.chart.options.chart.ignoreHiddenSeries !== false)) {
  387. return;
  388. }
  389. var series = this, xData = series.processedXData, yData = series.processedYData, stackedYData = [], yDataLength = yData.length, seriesOptions = series.options, threshold = seriesOptions.threshold, stackThreshold = pick(seriesOptions.startFromThreshold && threshold, 0), stackOption = seriesOptions.stack, stacking = seriesOptions.stacking, stackKey = series.stackKey, negKey = '-' + stackKey, negStacks = series.negStacks, yAxis = series.yAxis, stacks = yAxis.stacks, oldStacks = yAxis.oldStacks, stackIndicator, isNegative, stack, other, key, pointKey, i, x, y;
  390. yAxis.stacksTouched += 1;
  391. // loop over the non-null y values and read them into a local array
  392. for (i = 0; i < yDataLength; i++) {
  393. x = xData[i];
  394. y = yData[i];
  395. stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
  396. pointKey = stackIndicator.key;
  397. // Read stacked values into a stack based on the x value,
  398. // the sign of y and the stack key. Stacking is also handled for null
  399. // values (#739)
  400. isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
  401. key = isNegative ? negKey : stackKey;
  402. // Create empty object for this stack if it doesn't exist yet
  403. if (!stacks[key]) {
  404. stacks[key] =
  405. {};
  406. }
  407. // Initialize StackItem for this x
  408. if (!stacks[key][x]) {
  409. if (oldStacks[key] &&
  410. oldStacks[key][x]) {
  411. stacks[key][x] = oldStacks[key][x];
  412. stacks[key][x].total = null;
  413. }
  414. else {
  415. stacks[key][x] = new H.StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption);
  416. }
  417. }
  418. // If the StackItem doesn't exist, create it first
  419. stack = stacks[key][x];
  420. if (y !== null) {
  421. stack.points[pointKey] = stack.points[series.index] =
  422. [pick(stack.cumulative, stackThreshold)];
  423. // Record the base of the stack
  424. if (!defined(stack.cumulative)) {
  425. stack.base = pointKey;
  426. }
  427. stack.touched = yAxis.stacksTouched;
  428. // In area charts, if there are multiple points on the same X value,
  429. // let the area fill the full span of those points
  430. if (stackIndicator.index > 0 && series.singleStacks === false) {
  431. stack.points[pointKey][0] =
  432. stack.points[series.index + ',' + x + ',0'][0];
  433. }
  434. // When updating to null, reset the point stack (#7493)
  435. }
  436. else {
  437. stack.points[pointKey] = stack.points[series.index] =
  438. null;
  439. }
  440. // Add value to the stack total
  441. if (stacking === 'percent') {
  442. // Percent stacked column, totals are the same for the positive and
  443. // negative stacks
  444. other = isNegative ? stackKey : negKey;
  445. if (negStacks && stacks[other] && stacks[other][x]) {
  446. other = stacks[other][x];
  447. stack.total = other.total =
  448. Math.max(other.total, stack.total) +
  449. Math.abs(y) ||
  450. 0;
  451. // Percent stacked areas
  452. }
  453. else {
  454. stack.total =
  455. correctFloat(stack.total + (Math.abs(y) || 0));
  456. }
  457. }
  458. else {
  459. stack.total = correctFloat(stack.total + (y || 0));
  460. }
  461. stack.cumulative =
  462. pick(stack.cumulative, stackThreshold) + (y || 0);
  463. if (y !== null) {
  464. stack.points[pointKey].push(stack.cumulative);
  465. stackedYData[i] = stack.cumulative;
  466. }
  467. }
  468. if (stacking === 'percent') {
  469. yAxis.usePercentage = true;
  470. }
  471. this.stackedYData = stackedYData; // To be used in getExtremes
  472. // Reset old stacks
  473. yAxis.oldStacks = {};
  474. };
  475. /**
  476. * Iterate over all stacks and compute the absolute values to percent
  477. *
  478. * @private
  479. * @function Highcharts.Series#modifyStacks
  480. * @return {void}
  481. */
  482. Series.prototype.modifyStacks = function () {
  483. var series = this, stackKey = series.stackKey, stacks = series.yAxis.stacks, processedXData = series.processedXData, stackIndicator, stacking = series.options.stacking;
  484. if (series[stacking + 'Stacker']) { // Modifier function exists
  485. [stackKey, '-' + stackKey].forEach(function (key) {
  486. var i = processedXData.length, x, stack, pointExtremes;
  487. while (i--) {
  488. x = processedXData[i];
  489. stackIndicator = series.getStackIndicator(stackIndicator, x, series.index, key);
  490. stack = stacks[key] && stacks[key][x];
  491. pointExtremes =
  492. stack && stack.points[stackIndicator.key];
  493. if (pointExtremes) {
  494. series[stacking + 'Stacker'](pointExtremes, stack, i);
  495. }
  496. }
  497. });
  498. }
  499. };
  500. /**
  501. * Modifier function for percent stacks. Blows up the stack to 100%.
  502. *
  503. * @private
  504. * @function Highcharts.Series#percentStacker
  505. * @param {Array<number>} pointExtremes
  506. * @param {Highcharts.StackItem} stack
  507. * @param {number} i
  508. * @return {void}
  509. */
  510. Series.prototype.percentStacker = function (pointExtremes, stack, i) {
  511. var totalFactor = stack.total ? 100 / stack.total : 0;
  512. // Y bottom value
  513. pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor);
  514. // Y value
  515. pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor);
  516. this.stackedYData[i] = pointExtremes[1];
  517. };
  518. /**
  519. * Get stack indicator, according to it's x-value, to determine points with the
  520. * same x-value
  521. *
  522. * @private
  523. * @function Highcharts.Series#getStackIndicator
  524. * @param {Highcharts.StackItemIndicatorObject|undefined} stackIndicator
  525. * @param {number} x
  526. * @param {number} index
  527. * @param {string} [key]
  528. * @return {Highcharts.StackItemIndicatorObject}
  529. */
  530. Series.prototype.getStackIndicator = function (stackIndicator, x, index, key) {
  531. // Update stack indicator, when:
  532. // first point in a stack || x changed || stack type (negative vs positive)
  533. // changed:
  534. if (!defined(stackIndicator) ||
  535. stackIndicator.x !== x ||
  536. (key && stackIndicator.key !== key)) {
  537. stackIndicator = {
  538. x: x,
  539. index: 0,
  540. key: key
  541. };
  542. }
  543. else {
  544. stackIndicator.index++;
  545. }
  546. stackIndicator.key =
  547. [index, x, stackIndicator.index].join(',');
  548. return stackIndicator;
  549. };