Stacking.js 17 KB

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