1/* * 2 * 3 * (c) 2010-2024 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'; 11import A from '../../Animation/AnimationUtilities.js'; 12const { getDeferredAnimation } = A; 13import Axis from '../Axis.js'; 14import SeriesRegistry from '../../Series/SeriesRegistry.js'; 15const { series: { prototype: seriesProto } } = SeriesRegistry; 16import StackItem from './StackItem.js'; 17import U from '../../Utilities.js'; 18const { addEvent, correctFloat, defined, destroyObjectProperties, fireEvent, isNumber, objectEach, pick } = U; 19/* * 20 * 21 * Functions 22 * 23 * */ 24/** 25 * Generate stacks for each series and calculate stacks total values 26 * 27 * @private 28 * @function Highcharts.Chart#getStacks 29 */ 30function chartGetStacks() { 31 const chart = this, inverted = chart.inverted; 32 // Reset stacks for each axis 33 chart.axes.forEach((axis) => { 34 if (axis.stacking && axis.stacking.stacks && axis.hasVisibleSeries) { 35 axis.stacking.oldStacks = axis.stacking.stacks; 36 } 37 }); 38 chart.series.forEach((series) => { 39 const xAxisOptions = series.xAxis && series.xAxis.options || {}; 40 if (series.options.stacking && series.reserveSpace()) { 41 series.stackKey = [ 42 series.type, 43 pick(series.options.stack, ''), 44 inverted ? xAxisOptions.top : xAxisOptions.left, 45 inverted ? xAxisOptions.height : xAxisOptions.width 46 ].join(','); 47 } 48 }); 49} 50/** 51 * @private 52 */ 53function onAxisDestroy() { 54 const stacking = this.stacking; 55 if (stacking) { 56 const stacks = stacking.stacks; 57 // Destroy each stack total 58 objectEach(stacks, (stack, stackKey) => { 59 destroyObjectProperties(stack); 60 delete stacks[stackKey]; 61 }); 62 stacking.stackTotalGroup?.destroy(); 63 } 64} 65/** 66 * @private 67 */ 68function onAxisInit() { 69 if (!this.stacking) { 70 this.stacking = new AxisAdditions(this); 71 } 72} 73/** 74 * Get stack indicator, according to it's x-value, to determine points with the 75 * same x-value 76 * 77 * @private 78 * @function Highcharts.Series#getStackIndicator 79 */ 80function seriesGetStackIndicator(stackIndicator, x, index, key) { 81 // Update stack indicator, when: 82 // first point in a stack || x changed || stack type (negative vs positive) 83 // changed: 84 if (!defined(stackIndicator) || 85 stackIndicator.x !== x || 86 (key && stackIndicator.stackKey !== key)) { 87 stackIndicator = { 88 x: x, 89 index: 0, 90 key, 91 stackKey: key 92 }; 93 } 94 else { 95 stackIndicator.index++; 96 } 97 stackIndicator.key = [index, x, stackIndicator.index].join(','); 98 return stackIndicator; 99} 100/** 101 * Iterate over all stacks and compute the absolute values to percent 102 * 103 * @private 104 * @function Highcharts.Series#modifyStacks 105 */ 106function seriesModifyStacks() { 107 const series = this, yAxis = series.yAxis, stackKey = series.stackKey || '', stacks = yAxis.stacking.stacks, processedXData = series.getColumn('x', true), stacking = series.options.stacking, stacker = series[stacking + 'Stacker']; 108 let stackIndicator; 109 if (stacker) { // Modifier function exists (Series.percentStacker etc.) 110 [stackKey, '-' + stackKey].forEach((key) => { 111 let i = processedXData.length, x, stackItem, pointExtremes; 112 while (i--) { 113 x = processedXData[i]; 114 stackIndicator = series.getStackIndicator(stackIndicator, x, series.index, key); 115 stackItem = stacks[key]?.[x]; 116 pointExtremes = stackItem?.points[stackIndicator.key || '']; 117 if (pointExtremes) { 118 stacker.call(series, pointExtremes, stackItem, i); 119 } 120 } 121 }); 122 } 123} 124/** 125 * Modifier function for percent stacks. Blows up the stack to 100%. 126 * 127 * @private 128 * @function Highcharts.Series#percentStacker 129 */ 130function seriesPercentStacker(pointExtremes, stack, i) { 131 const totalFactor = stack.total ? 100 / stack.total : 0; 132 // Y bottom value 133 pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); 134 // Y value 135 pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); 136 this.stackedYData[i] = pointExtremes[1]; 137} 138/** 139 * Set grouped points in a stack-like object. When `centerInCategory` is true, 140 * and `stacking` is not enabled, we need a pseudo (horizontal) stack in order 141 * to handle grouping of points within the same category. 142 * 143 * @private 144 * @function Highcharts.Series#setGroupedPoints 145 * @return {void} 146 */ 147function seriesSetGroupedPoints(axis) { 148 // Only series types supporting centerInCategory need to do this. That also 149 // applies to resetting (#20221). 150 if (this.is('column') || this.is('columnrange')) { 151 if (this.options.centerInCategory &&
vendor: 5,424 bytes, lines 152-288
152 // With only one series, we don't need to consider centerInCategory 153 this.chart.series.length > 1) { 154 seriesProto.setStackedPoints.call(this, axis, 'group'); 155 // After updating, if we now have proper stacks, we must delete the 156 // group pseudo stacks (#14980) 157 } 158 else { 159 axis.stacking.resetStacks(); 160 } 161 } 162} 163/** 164 * Adds series' points value to corresponding stack 165 * 166 * @private 167 * @function Highcharts.Series#setStackedPoints 168 */ 169function seriesSetStackedPoints(axis, stackingParam) { 170 const type = stackingParam || this.options.stacking; 171 if (!type || 172 !this.reserveSpace() || 173 // Group stacks (centerInCategory) belong on the x-axis, other stacks on 174 // the y-axis. 175 ({ group: 'xAxis' }[type] || 'yAxis') !== axis.coll) { 176 return; 177 } 178 const series = this, xData = series.getColumn('x', true), yData = series.getColumn(series.pointValKey || 'y', true), stackedYData = [], yDataLength = yData.length, seriesOptions = series.options, threshold = seriesOptions.threshold || 0, stackThreshold = seriesOptions.startFromThreshold ? threshold : 0, stackOption = seriesOptions.stack, stackKey = stackingParam ? 179 `${series.type},${type}` : (series.stackKey || ''), negKey = '-' + stackKey, negStacks = series.negStacks, stacking = axis.stacking, stacks = stacking.stacks, oldStacks = stacking.oldStacks; 180 let stackIndicator, isNegative, stack, other, key, pointKey, i; 181 stacking.stacksTouched += 1; 182 // Loop over the non-null y values and read them into a local array 183 for (i = 0; i < yDataLength; i++) { 184 const x = xData[i] || 0, y = yData[i], yNumber = isNumber(y) && y || 0; 185 stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); 186 pointKey = stackIndicator.key || ''; 187 // Read stacked values into a stack based on the x value, 188 // the sign of y and the stack key. Stacking is also handled for null 189 // values (#739) 190 isNegative = negStacks && yNumber < (stackThreshold ? 0 : threshold); 191 key = isNegative ? negKey : stackKey; 192 // Create empty object for this stack if it doesn't exist yet 193 if (!stacks[key]) { 194 stacks[key] = {}; 195 } 196 // Initialize StackItem for this x 197 if (!stacks[key][x]) { 198 if (oldStacks[key]?.[x]) { 199 stacks[key][x] = oldStacks[key][x]; 200 stacks[key][x].total = null; 201 } 202 else { 203 stacks[key][x] = new StackItem(axis, axis.options.stackLabels, !!isNegative, x, stackOption); 204 } 205 } 206 // If the StackItem doesn't exist, create it first 207 stack = stacks[key][x]; 208 if (y !== null) { 209 stack.points[pointKey] = stack.points[series.index] = [ 210 pick(stack.cumulative, stackThreshold) 211 ]; 212 // Record the base of the stack 213 if (!defined(stack.cumulative)) { 214 stack.base = pointKey; 215 } 216 stack.touched = stacking.stacksTouched; 217 // In area charts, if there are multiple points on the same X value, 218 // let the area fill the full span of those points 219 if (stackIndicator.index > 0 && series.singleStacks === false) { 220 stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0]; 221 } 222 // When updating to null, reset the point stack (#7493) 223 } 224 else { 225 delete stack.points[pointKey]; 226 delete stack.points[series.index]; 227 } 228 // Add value to the stack total 229 let total = stack.total || 0; 230 if (type === 'percent') { 231 // Percent stacked column, totals are the same for the positive and 232 // negative stacks 233 other = isNegative ? stackKey : negKey; 234 if (negStacks && stacks[other]?.[x]) { 235 other = stacks[other][x]; 236 total = other.total = Math.max(other.total || 0, total) + 237 Math.abs(yNumber); 238 // Percent stacked areas 239 } 240 else { 241 total = correctFloat(total + Math.abs(yNumber)); 242 } 243 } 244 else if (type === 'group') { 245 // In this stack, the total is the number of valid points 246 if (isNumber(y)) { 247 total++; 248 } 249 } 250 else { 251 total = correctFloat(total + yNumber); 252 } 253 if (type === 'group') { 254 // This point's index within the stack, pushed to stack.points[1] 255 stack.cumulative = (total || 1) - 1; 256 } 257 else { 258 stack.cumulative = correctFloat(pick(stack.cumulative, stackThreshold) + yNumber); 259 } 260 stack.total = total; 261 if (y !== null) { 262 stack.points[pointKey].push(stack.cumulative); 263 stackedYData[i] = stack.cumulative; 264 stack.hasValidPoints = true; 265 } 266 } 267 if (type === 'percent') { 268 stacking.usePercentage = true; 269 } 270 if (type !== 'group') { 271 this.stackedYData = stackedYData; // To be used in getExtremes 272 } 273 // Reset old stacks 274 stacking.oldStacks = {}; 275} 276/* * 277 * 278 * Classes 279 * 280 * */ 281/** 282 * Adds stacking support to axes. 283 * @private 284 * @class 285 */ 286class AxisAdditions { 287 /* * 288 *
289 * Constructors 290 * 291 * */ 292 constructor(axis) { 293 this.oldStacks = {}; 294 this.stacks = {}; 295 this.stacksTouched = 0; 296 this.axis = axis; 297 } 298 /* * 299 * 300 * Functions 301 * 302 * */ 303 /** 304 * Build the stacks from top down 305 * @private 306 */ 307 buildStacks() { 308 const stacking = this, axis = stacking.axis, axisSeries = axis.series, isXAxis = axis.coll === 'xAxis', reversedStacks = axis.options.reversedStacks, len = axisSeries.length; 309 let actualSeries, i; 310 this.resetStacks(); 311 stacking.usePercentage = false; 312 i = len; 313 while (i--) { 314 actualSeries = axisSeries[reversedStacks ? i : len - i - 1]; 315 if (isXAxis) { 316 actualSeries.setGroupedPoints(axis); 317 } 318 actualSeries.setStackedPoints(axis); 319 } 320 // Loop up again to compute percent and stream stack 321 if (!isXAxis) { 322 for (i = 0; i < len; i++) { 323 axisSeries[i].modifyStacks(); 324 } 325 } 326 fireEvent(axis, 'afterBuildStacks'); 327 } 328 /** 329 * @private 330 */ 331 cleanStacks() { 332 if (this.oldStacks) { 333 this.stacks = this.oldStacks; 334 // Reset stacks 335 objectEach(this.stacks, (type) => { 336 objectEach(type, (stack) => { 337 stack.cumulative = stack.total; 338 }); 339 }); 340 } 341 } 342 /** 343 * Set all the stacks to initial states and destroy unused ones. 344 * @private 345 */ 346 resetStacks() { 347 objectEach(this.stacks, (type) => { 348 objectEach(type, (stack, x) => { 349 // Clean up memory after point deletion (#1044, #4320) 350 if (isNumber(stack.touched) && 351 stack.touched < this.stacksTouched) { 352 stack.destroy(); 353 delete type[x]; 354 // Reset stacks 355 } 356 else { 357 stack.total = null; 358 stack.cumulative = null; 359 } 360 }); 361 }); 362 } 363 /** 364 * @private 365 */ 366 renderStackTotals() { 367 const stacking = this, axis = stacking.axis, chart = axis.chart, renderer = chart.renderer, stacks = stacking.stacks, stackLabelsAnim = axis.options.stackLabels?.animation, animationConfig = getDeferredAnimation(chart, stackLabelsAnim || false), stackTotalGroup = stacking.stackTotalGroup = (stacking.stackTotalGroup || 368 renderer 369 .g('stack-labels') 370 .attr({ 371 zIndex: 6, 372 opacity: 0 373 }) 374 .add()); 375 // The plotLeft/Top will change when y axis gets wider so we need to 376 // translate the stackTotalGroup at every render call. See bug #506 377 // and #516 378 stackTotalGroup.translate(chart.plotLeft, chart.plotTop); 379 // Render each stack total 380 objectEach(stacks, (type) => { 381 objectEach(type, (stack) => { 382 stack.render(stackTotalGroup); 383 }); 384 }); 385 stackTotalGroup.animate({ 386 opacity: 1 387 }, animationConfig); 388 } 389} 390/* * 391 * 392 * Composition 393 * 394 * */ 395var StackingAxis; 396(function (StackingAxis) { 397 /* * 398 * 399 * Functions 400 * 401 * */ 402 /** 403 * Extends axis with stacking support. 404 * @private 405 */ 406 function compose(AxisClass, ChartClass, SeriesClass) { 407 const chartProto = ChartClass.prototype, seriesProto = SeriesClass.prototype; 408 if (!chartProto.getStacks) { 409 addEvent(AxisClass, 'init', onAxisInit); 410 addEvent(AxisClass, 'destroy', onAxisDestroy); 411 chartProto.getStacks = chartGetStacks; 412 seriesProto.getStackIndicator = seriesGetStackIndicator; 413 seriesProto.modifyStacks = seriesModifyStacks; 414 seriesProto.percentStacker = seriesPercentStacker; 415 seriesProto.setGroupedPoints = seriesSetGroupedPoints; 416 seriesProto.setStackedPoints = seriesSetStackedPoints; 417 } 418 } 419 StackingAxis.compose = compose; 420})(StackingAxis || (StackingAxis = {})); 421/* * 422 * 423 * Default Export 424 * 425 * */ 426export default StackingAxis;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.