Stacking.js 19 KB

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