Stacking.js 20 KB

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