Stacking.js 19 KB

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