Stacking.js 20 KB

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