UNPKG

apexcharts

Version:

A JavaScript Chart Library

956 lines (886 loc) 30.5 kB
// @ts-check import Utils from '../utils/Utils' import DateTime from '../utils/DateTime' import Scales from './Scales' /** * Range is used to generates values between min and max. * * @module Range **/ class Range { /** * @param {import('../types/internal').ChartStateW} w */ constructor(w) { this.w = w this.scales = new Scales(this.w) } init() { this.setYRange() this.setXRange() this.setZRange() } /** * @param {number} startingSeriesIndex * @param {number | null} [endingSeriesIndex] */ getMinYMaxY( startingSeriesIndex, lowestY = Number.MAX_VALUE, highestY = -Number.MAX_VALUE, endingSeriesIndex = null, ) { const cnf = this.w.config const gl = this.w.globals let maxY = -Number.MAX_VALUE let minY = Number.MIN_VALUE if (endingSeriesIndex === null) { endingSeriesIndex = startingSeriesIndex + 1 } const series = this.w.seriesData.series let seriesMin = series let seriesMax = series if (cnf.chart.type === 'candlestick') { seriesMin = this.w.candleData.seriesCandleL seriesMax = this.w.candleData.seriesCandleH } else if (cnf.chart.type === 'boxPlot') { seriesMin = this.w.candleData.seriesCandleO seriesMax = this.w.candleData.seriesCandleC } else if (cnf.chart.type === 'violin') { seriesMin = this.w.violinData.seriesViolinMin seriesMax = this.w.violinData.seriesViolinMax } else if (this.w.axisFlags.isRangeData) { seriesMin = this.w.rangeData.seriesRangeStart seriesMax = this.w.rangeData.seriesRangeEnd } let autoScaleYaxis = false if (this.w.seriesData.seriesX.length >= endingSeriesIndex) { // Eventually brushSource will be set if the current chart is a target. // That is, after the appropriate event causes us to update. const brush = /** @type {any} */ (gl).brushSource?.w.config.chart.brush if ( (cnf.chart.zoom.enabled && cnf.chart.zoom.autoScaleYaxis) || (brush?.enabled && brush?.autoScaleYaxis) ) { autoScaleYaxis = true } } for (let i = startingSeriesIndex; i < endingSeriesIndex; i++) { gl.dataPoints = Math.max(gl.dataPoints, series[i].length) const seriesType = /** @type {Record<string,any>} */ (cnf.series[i]).type if (this.w.labelData.categoryLabels.length) { /** * @param {string} label */ gl.dataPoints = this.w.labelData.categoryLabels.filter( (label) => typeof label !== 'undefined', ).length } if ( this.w.labelData.labels.length && cnf.xaxis.type !== 'datetime' && /** * @param {number} a * @param {number[]} c */ this.w.seriesData.series.reduce((a, c) => a + c.length, 0) !== 0 ) { // the condition cnf.xaxis.type !== 'datetime' fixes #3897 and #3905 gl.dataPoints = Math.max(gl.dataPoints, this.w.labelData.labels.length) } let firstXIndex = 0 let lastXIndex = series[i].length - 1 if (autoScaleYaxis) { // Scale the Y axis to the min..max within the possibly zoomed X axis domain. if (cnf.xaxis.min) { for ( ; firstXIndex < lastXIndex && this.w.seriesData.seriesX[i][firstXIndex] < cnf.xaxis.min; firstXIndex++ ) { // Intentionally empty - just incrementing firstXIndex } } if (cnf.xaxis.max) { for ( ; lastXIndex > firstXIndex && this.w.seriesData.seriesX[i][lastXIndex] > cnf.xaxis.max; lastXIndex-- ) { // Intentionally empty - just decrementing lastXIndex } } } // Fast lane for plain numeric series (line/area/scatter/column with // scalar y values): the general loop below pays several function calls // and a string conversion PER VALUE (isNumber/noExponents/isFloat/ // toString), measured as the largest post-parse CPU bucket at 50k+ // points. This lane replicates its exact observable effects (min/max, // hasNullValues, yValueDecimal, negative minY) with inline checks. const plainNumeric = seriesMin === series && seriesMax === series && cnf.chart.type !== 'boxPlot' && seriesType !== 'candlestick' && seriesType !== 'boxPlot' && seriesType !== 'violin' && seriesType !== 'rangeArea' && seriesType !== 'rangeBar' && !( this.w.seriesData.seriesGoals[i] && this.w.seriesData.seriesGoals[i].length ) if (plainNumeric) { const arr = series[i] const jEnd = Math.min(lastXIndex, arr.length - 1) const pe = this.w.seriesData._parsedExtrema?.[i] if ( pe && pe.ref === arr && pe.len === arr.length && firstXIndex === 0 && jEnd === arr.length - 1 ) { // extrema already fused into the parse pass for this exact array // and the scan window is the full series: merge, skip the loop if (pe.maxY > maxY) maxY = pe.maxY if (pe.lowestY < lowestY) lowestY = pe.lowestY if (pe.negMinY < 0 && pe.negMinY < minY) minY = pe.negMinY if (pe.yDec > gl.yValueDecimal) gl.yValueDecimal = pe.yDec if (pe.hasNulls) gl.hasNullValues = true } else { let yDec = gl.yValueDecimal let hasNulls = false for (let j = firstXIndex; j <= jEnd; j++) { const val = arr[j] // inline isNumber: number, not NaN/Infinity, not null if ( val !== null && typeof val === 'number' && val === val && val !== Infinity && val !== -Infinity ) { if (val > maxY) maxY = val if (val < lowestY) lowestY = val if (minY > val && val < 0) minY = val if (!Number.isInteger(val)) { const av = val < 0 ? -val : val if (av >= 1e-6 && av < 1e21) { // plain decimal notation: count fraction digits directly const str = '' + val const dot = str.indexOf('.') const dec = dot === -1 ? 0 : str.length - dot - 1 if (dec > yDec) yDec = dec } else { // exponent notation: use the exact legacy conversion const nv = Utils.noExponents(val) if (Utils.isFloat(nv)) { yDec = Math.max(yDec, nv.toString().split('.')[1].length) } } } } else { hasNulls = true } } gl.yValueDecimal = yDec if (hasNulls) gl.hasNullValues = true } highestY = maxY // bar/column post-loop adjustments (same as the general loop's tail) if (seriesType === 'bar' || seriesType === 'column') { if (minY < 0 && maxY < 0) { maxY = 0 highestY = Math.max(highestY, 0) } if (minY === Number.MIN_VALUE) { minY = 0 lowestY = Math.min(lowestY, 0) } } continue } for ( let j = firstXIndex; j <= lastXIndex && j < this.w.seriesData.series[i].length; j++ ) { let val = series[i][j] if (val !== null && Utils.isNumber(val)) { if (typeof seriesMax[i]?.[j] !== 'undefined') { maxY = Math.max(maxY, seriesMax[i][j]) lowestY = Math.min(lowestY, seriesMax[i][j]) } if (typeof seriesMin[i]?.[j] !== 'undefined') { lowestY = Math.min(lowestY, seriesMin[i][j]) highestY = Math.max(highestY, seriesMin[i][j]) } // These series arrays are dual purpose: // Array : CandleO, CandleH, CandleM, CandleL, CandleC // Candlestick: O H L C // Boxplot : Min Q1 Median Q3 Max switch (seriesType) { case 'candlestick': { if ( typeof this.w.candleData.seriesCandleC[i][j] !== 'undefined' ) { maxY = Math.max(maxY, this.w.candleData.seriesCandleH[i][j]) lowestY = Math.min( lowestY, this.w.candleData.seriesCandleL[i][j], ) } } break case 'boxPlot': { if ( typeof this.w.candleData.seriesCandleC[i][j] !== 'undefined' ) { maxY = Math.max(maxY, this.w.candleData.seriesCandleC[i][j]) lowestY = Math.min( lowestY, this.w.candleData.seriesCandleO[i][j], ) } } break case 'violin': { if ( typeof this.w.violinData.seriesViolinMax[i]?.[j] !== 'undefined' ) { maxY = Math.max(maxY, this.w.violinData.seriesViolinMax[i][j]) lowestY = Math.min( lowestY, this.w.violinData.seriesViolinMin[i][j], ) } } break } // there is a combo chart and the specified series in not either // candlestick, boxplot, or rangeArea/rangeBar; find the max there. if ( seriesType && seriesType !== 'candlestick' && seriesType !== 'boxPlot' && seriesType !== 'violin' && seriesType !== 'rangeArea' && seriesType !== 'rangeBar' ) { maxY = Math.max(maxY, this.w.seriesData.series[i][j]) lowestY = Math.min(lowestY, this.w.seriesData.series[i][j]) } if ( this.w.seriesData.seriesGoals[i] && this.w.seriesData.seriesGoals[i][j] && Array.isArray(this.w.seriesData.seriesGoals[i][j]) ) { this.w.seriesData.seriesGoals[i][j].forEach( (/** @type {any} */ g) => { maxY = Math.max(maxY, g.value) lowestY = Math.min(lowestY, g.value) }, ) } // boxPlot raw observations (optional jitter) — included regardless of // how seriesType resolves, so outliers beyond the box min/max stay in // the axis extent. if (this.w.config.chart.type === 'boxPlot' || seriesType === 'boxPlot') { const boxPts = this.w.candleData.seriesBoxPoints?.[i]?.[j] if (boxPts) { for (let p = 0; p < boxPts.length; p++) { const pv = boxPts[p] if (typeof pv === 'number') { maxY = Math.max(maxY, pv) lowestY = Math.min(lowestY, pv) } } } } highestY = maxY val = Utils.noExponents(val) if (Utils.isFloat(val)) { gl.yValueDecimal = Math.max( gl.yValueDecimal, val.toString().split('.')[1].length, ) } if (minY > seriesMin[i]?.[j] && seriesMin[i]?.[j] < 0) { minY = seriesMin[i][j] } } else { gl.hasNullValues = true } } if (seriesType === 'bar' || seriesType === 'column') { if (minY < 0 && maxY < 0) { // all negative values in a bar series, hence make the max to 0 maxY = 0 highestY = Math.max(highestY, 0) } if (minY === Number.MIN_VALUE) { minY = 0 lowestY = Math.min(lowestY, 0) } } } if ( cnf.chart.type === 'rangeBar' && this.w.rangeData.seriesRangeStart.length && gl.isBarHorizontal ) { minY = lowestY } if (cnf.chart.type === 'bar') { if (minY < 0 && maxY < 0) { // all negative values in a bar chart, hence make the max to 0 maxY = 0 } if (minY === Number.MIN_VALUE) { minY = 0 } } return { minY, maxY, lowestY, highestY, } } setYRange() { const gl = this.w.globals const cnf = this.w.config gl.maxY = -Number.MAX_VALUE gl.minY = Number.MIN_VALUE let lowestYInAllSeries = Number.MAX_VALUE let minYMaxY if (gl.isMultipleYAxis) { // we need to get minY and maxY for multiple y axis lowestYInAllSeries = Number.MAX_VALUE for (let i = 0; i < this.w.seriesData.series.length; i++) { minYMaxY = this.getMinYMaxY(i) gl.minYArr[i] = minYMaxY.lowestY gl.maxYArr[i] = minYMaxY.highestY lowestYInAllSeries = Math.min(lowestYInAllSeries, minYMaxY.lowestY) } } // and then, get the minY and maxY from all series minYMaxY = this.getMinYMaxY( 0, lowestYInAllSeries, undefined, this.w.seriesData.series.length, ) if (cnf.chart.type === 'bar') { gl.minY = minYMaxY.minY gl.maxY = minYMaxY.maxY } else { gl.minY = minYMaxY.lowestY gl.maxY = minYMaxY.highestY } lowestYInAllSeries = minYMaxY.lowestY if (cnf.chart.stacked) { this._setStackedMinMax() } // if the numbers are too big, reduce the range // for eg, if number is between 100000-110000, putting 0 as the lowest // value is not so good idea. So change the gl.minY for // line/area/scatter/candlesticks/boxPlot/vertical rangebar if ( cnf.chart.type === 'line' || cnf.chart.type === 'area' || cnf.chart.type === 'scatter' || cnf.chart.type === 'candlestick' || cnf.chart.type === 'boxPlot' || cnf.chart.type === 'violin' || (cnf.chart.type === 'rangeBar' && !gl.isBarHorizontal) ) { if ( gl.minY === Number.MIN_VALUE && lowestYInAllSeries !== -Number.MAX_VALUE && lowestYInAllSeries !== gl.maxY // single value possibility ) { gl.minY = lowestYInAllSeries } } else { gl.minY = gl.minY !== Number.MIN_VALUE ? Math.min(minYMaxY.minY, gl.minY) : minYMaxY.minY } /** * @param {ApexYAxis} yaxe * @param {number} index */ cnf.yaxis.forEach((yaxe, index) => { // override all min/max values by user defined values (y axis) if (yaxe.max !== undefined) { if (typeof yaxe.max === 'number') { gl.maxYArr[index] = yaxe.max } else if (typeof yaxe.max === 'function') { // fixes apexcharts.js/issues/2098 gl.maxYArr[index] = yaxe.max( gl.isMultipleYAxis ? gl.maxYArr[index] : gl.maxY, ) } // gl.maxY is for single y-axis chart, it will be ignored in multi-yaxis gl.maxY = gl.maxYArr[index] } if (yaxe.min !== undefined) { if (typeof yaxe.min === 'number') { gl.minYArr[index] = yaxe.min } else if (typeof yaxe.min === 'function') { // fixes apexcharts.js/issues/2098 gl.minYArr[index] = yaxe.min( gl.isMultipleYAxis ? gl.minYArr[index] === Number.MIN_VALUE ? 0 : gl.minYArr[index] : gl.minY, ) } // gl.minY is for single y-axis chart, it will be ignored in multi-yaxis gl.minY = gl.minYArr[index] } }) // for horizontal bar charts, we need to check xaxis min/max as user may have specified there if (gl.isBarHorizontal) { const minmax = ['min', 'max'] minmax.forEach((m) => { if (cnf.xaxis[m] !== undefined && typeof cnf.xaxis[m] === 'number') { m === 'min' ? (gl.minY = cnf.xaxis[m]) : (gl.maxY = cnf.xaxis[m]) } }) } if (gl.isMultipleYAxis) { this.scales.scaleMultipleYAxes() gl.minY = lowestYInAllSeries } else { this.scales.setYScaleForIndex(0, gl.minY, gl.maxY) gl.minY = gl.yAxisScale[0].niceMin gl.maxY = gl.yAxisScale[0].niceMax gl.minYArr[0] = gl.minY gl.maxYArr[0] = gl.maxY } gl.barGroups = [] gl.lineGroups = [] gl.areaGroups = [] /** * @param {Record<string, any>} s */ cnf.series.forEach((s) => { const _s = /** @type {any} */ (s) const type = _s.type || cnf.chart.type switch (type) { case 'bar': case 'column': gl.barGroups.push(_s.group) break case 'line': gl.lineGroups.push(_s.group) break case 'area': gl.areaGroups.push(_s.group) break } }) // Uniquify the group names in each stackable chart type. /** * @param {string} v * @param {number} i * @param {string[]} a */ gl.barGroups = gl.barGroups.filter((v, i, a) => a.indexOf(v) === i) /** * @param {string} v * @param {number} i * @param {string[]} a */ gl.lineGroups = gl.lineGroups.filter((v, i, a) => a.indexOf(v) === i) /** * @param {string} v * @param {number} i * @param {string[]} a */ gl.areaGroups = gl.areaGroups.filter((v, i, a) => a.indexOf(v) === i) return { minY: gl.minY, maxY: gl.maxY, minYArr: gl.minYArr, maxYArr: gl.maxYArr, yAxisScale: gl.yAxisScale, } } setXRange() { const gl = this.w.globals const cnf = this.w.config const isXNumeric = cnf.xaxis.type === 'numeric' || cnf.xaxis.type === 'datetime' || (cnf.xaxis.type === 'category' && !this.w.axisFlags.noLabelsProvided) || this.w.axisFlags.noLabelsProvided || this.w.axisFlags.isXNumeric const getInitialMinXMaxX = () => { // hot loop at 50k+ points: track locals with inline number checks and // write the globals once (initialMin/MaxX always mirror min/maxX here) let minX = gl.minX let maxX = gl.maxX for (let i = 0; i < this.w.seriesData.series.length; i++) { const lbls = /** @type {any} */ (this.w.labelData.labels[i]) if (!lbls) continue const pe = this.w.seriesData._parsedExtrema?.[i] if ( pe && pe.xNumeric && pe.xref === lbls && pe.len === lbls.length ) { // x extrema already fused into the parse pass for this exact array if (pe.maxX > maxX) maxX = pe.maxX if (pe.minX < minX) minX = pe.minX continue } for (let j = 0; j < lbls.length; j++) { const v = lbls[j] if (v !== null && typeof v === 'number' && v === v) { if (v > maxX) maxX = v if (v < minX) minX = v } } } gl.maxX = maxX gl.initialMaxX = maxX gl.minX = minX gl.initialMinX = minX } // minX maxX starts here if (this.w.axisFlags.isXNumeric) { getInitialMinXMaxX() } if (this.w.axisFlags.noLabelsProvided) { if (cnf.xaxis.categories.length === 0) { gl.maxX = /** @type {any} */ ( this.w.labelData.labels[this.w.labelData.labels.length - 1] ) gl.initialMaxX = /** @type {any} */ ( this.w.labelData.labels[this.w.labelData.labels.length - 1] ) gl.minX = 1 gl.initialMinX = 1 } } if ( this.w.axisFlags.isXNumeric || this.w.axisFlags.noLabelsProvided || this.w.axisFlags.dataFormatXNumeric ) { let ticks = 10 if (cnf.xaxis.tickAmount === undefined) { ticks = Math.round(gl.svgWidth / 150) // no labels provided and total number of dataPoints is less than 30 if (cnf.xaxis.type === 'numeric' && gl.dataPoints < 30) { ticks = gl.dataPoints - 1 } // this check is for when ticks exceeds total datapoints and that would result in duplicate labels if (ticks > gl.dataPoints && gl.dataPoints !== 0) { ticks = gl.dataPoints - 1 } } else if (cnf.xaxis.tickAmount === 'dataPoints') { if (this.w.seriesData.series.length > 1) { ticks = this.w.seriesData.series[gl.maxValsInArrayIndex].length - 1 } if (this.w.axisFlags.isXNumeric) { const diff = Math.round(gl.maxX - gl.minX) if (diff < 30) { // When numeric range is small, show a tick for every integer ticks = diff } } } else { ticks = cnf.xaxis.tickAmount } gl.xTickAmount = ticks // override all min/max values by user defined values (x axis) if (cnf.xaxis.max !== undefined && typeof cnf.xaxis.max === 'number') { gl.maxX = cnf.xaxis.max } if (cnf.xaxis.min !== undefined && typeof cnf.xaxis.min === 'number') { gl.minX = cnf.xaxis.min } // if range is provided, adjust the new minX if (cnf.xaxis.range !== undefined) { gl.minX = gl.maxX - cnf.xaxis.range } if (gl.minX !== Number.MAX_VALUE && gl.maxX !== -Number.MAX_VALUE) { if ( cnf.xaxis.convertedCatToNumeric && !this.w.axisFlags.dataFormatXNumeric ) { const catScale = [] for (let i = gl.minX - 1; i < gl.maxX; i++) { catScale.push(i + 1) } gl.xAxisScale = { result: catScale, niceMin: catScale[0], niceMax: catScale[catScale.length - 1], } } else { gl.xAxisScale = this.scales.setXScale(gl.minX, gl.maxX) } } else { gl.xAxisScale = this.scales.linearScale( 0, ticks, ticks, 0, cnf.xaxis.stepSize, ) if ( this.w.axisFlags.noLabelsProvided && this.w.labelData.labels.length > 0 ) { gl.xAxisScale = this.scales.linearScale( 1, this.w.labelData.labels.length, ticks - 1, 0, cnf.xaxis.stepSize, ) // this is the only place seriesX is again mutated this.w.seriesData.seriesX = /** @type {any} */ ( this.w.labelData.labels.slice() ) } } // we will still store these labels as the count for this will be different (to draw grid and labels placement) if (isXNumeric) { this.w.labelData.labels = /** @type {any} */ ( gl.xAxisScale.result.slice() ) } } if (gl.isBarHorizontal && this.w.labelData.labels.length) { gl.xTickAmount = this.w.labelData.labels.length } // single dataPoint this._handleSingleDataPoint() // minimum x difference to calculate bar width in numeric bars this._getMinXDiff() return { minX: gl.minX, maxX: gl.maxX, } } setZRange() { // minZ, maxZ starts here const gl = this.w.globals if (!this.w.axisFlags.isDataXYZ) return for (let i = 0; i < this.w.seriesData.series.length; i++) { if (typeof this.w.seriesData.seriesZ[i] !== 'undefined') { for (let j = 0; j < this.w.seriesData.seriesZ[i].length; j++) { if ( this.w.seriesData.seriesZ[i][j] !== null && Utils.isNumber(this.w.seriesData.seriesZ[i][j]) ) { gl.maxZ = Math.max(gl.maxZ, this.w.seriesData.seriesZ[i][j]) gl.minZ = Math.min(gl.minZ, this.w.seriesData.seriesZ[i][j]) } } } } } _handleSingleDataPoint() { const gl = this.w.globals const cnf = this.w.config if (gl.minX === gl.maxX) { const datetimeObj = new DateTime(this.w) if (cnf.xaxis.type === 'datetime') { const newMinX = datetimeObj.getDate(gl.minX) if (cnf.xaxis.labels.datetimeUTC) { newMinX.setUTCDate(newMinX.getUTCDate() - 2) } else { newMinX.setDate(newMinX.getDate() - 2) } gl.minX = new Date(newMinX).getTime() const newMaxX = datetimeObj.getDate(gl.maxX) if (cnf.xaxis.labels.datetimeUTC) { newMaxX.setUTCDate(newMaxX.getUTCDate() + 2) } else { newMaxX.setDate(newMaxX.getDate() + 2) } gl.maxX = new Date(newMaxX).getTime() } else if ( cnf.xaxis.type === 'numeric' || (cnf.xaxis.type === 'category' && !this.w.axisFlags.noLabelsProvided) ) { gl.minX = gl.minX - 2 gl.initialMinX = gl.minX gl.maxX = gl.maxX + 2 gl.initialMaxX = gl.maxX } } } _getMinXDiff() { const gl = this.w.globals if (this.w.axisFlags.isXNumeric) { // get the least x diff if numeric x axis is present /** * @param {number} sX * @param {number} si */ this.w.seriesData.seriesX.forEach((sX, si) => { if (sX.length) { if (sX.length === 1) { // a small hack to prevent overlapping multiple bars when there is just 1 datapoint in bar series. // fix #811 sX.push( this.w.seriesData.seriesX[gl.maxValsInArrayIndex][ this.w.seriesData.seriesX[gl.maxValsInArrayIndex].length - 1 ], ) } // sortedness + min positive diff already fused into the parse pass // for this exact array: merge and skip the scan const pe = this.w.seriesData._parsedExtrema?.[si] if ( pe && pe.xNumeric && pe.xSorted && pe.xref === sX && pe.len === sX.length ) { if (pe.minXDiff < gl.minXDiff) gl.minXDiff = pe.minXDiff if (gl.dataPoints === 1 || gl.minXDiff === Number.MAX_VALUE) { // fixes apexcharts.js #1221 gl.minXDiff = 0.5 } return } // Fast lane: x values are almost always already sorted (time // series, numeric streams). One pass detects sortedness and takes // the min positive diff without the O(n log n) clone + sort. let presorted = true let minDiff = gl.minXDiff for (let j = 1; j < sX.length; j++) { const d = sX[j] - sX[j - 1] if (d > 0) { if (d < minDiff) minDiff = d } else if (d < 0) { presorted = false break } } if (presorted) { gl.minXDiff = minDiff if (gl.dataPoints === 1 || gl.minXDiff === Number.MAX_VALUE) { // fixes apexcharts.js #1221 gl.minXDiff = 0.5 } return } // fix #983 (clone the array to avoid side effects) const seriesX = sX.slice() /** * @param {number} a * @param {number} b */ seriesX.sort((a, b) => a - b) /** * @param {Record<string, any>} s * @param {number} j */ seriesX.forEach((s, j) => { if (j > 0) { const xDiff = s - seriesX[j - 1] if (xDiff > 0) { gl.minXDiff = Math.min(xDiff, gl.minXDiff) } } }) if (gl.dataPoints === 1 || gl.minXDiff === Number.MAX_VALUE) { // fixes apexcharts.js #1221 gl.minXDiff = 0.5 } } }) } } _setStackedMinMax() { const gl = this.w.globals // for stacked charts, we calculate each series's parallel values. // i.e, series[0][j] + series[1][j] .... [series[i.length][j]] // and get the max out of it if (!this.w.seriesData.series.length) return let seriesGroups = this.w.labelData.seriesGroups if (!seriesGroups.length) { /** * @param {string} name */ seriesGroups = [this.w.seriesData.seriesNames.map((name) => name)] } /** @type {Record<string, number[]>} */ const stackedPoss = {} /** @type {Record<string, number[]>} */ const stackedNegs = {} seriesGroups.forEach((/** @type {any} */ group) => { stackedPoss[group] = [] stackedNegs[group] = [] const indicesOfSeriesInGroup = this.w.config.series .map((/** @type {any} */ serie, /** @type {any} */ si) => group.indexOf(this.w.seriesData.seriesNames[si]) > -1 ? si : null, ) .filter((/** @type {any} */ f) => f !== null) indicesOfSeriesInGroup.forEach((/** @type {number} */ i) => { for ( let j = 0; j < this.w.seriesData.series[gl.maxValsInArrayIndex].length; j++ ) { if (typeof stackedPoss[group][j] === 'undefined') { stackedPoss[group][j] = 0 stackedNegs[group][j] = 0 } const stackSeries = (this.w.config.chart.stacked && !gl.comboCharts) || (this.w.config.chart.stacked && gl.comboCharts && (!this.w.config.chart.stackOnlyBar || /** @type {Record<string,any>} */ (this.w.config.series?.[i]) ?.type === 'bar' || /** @type {Record<string,any>} */ (this.w.config.series?.[i]) ?.type === 'column')) if (stackSeries) { if ( this.w.seriesData.series[i][j] !== null && Utils.isNumber(this.w.seriesData.series[i][j]) ) { this.w.seriesData.series[i][j] > 0 ? (stackedPoss[group][j] += parseFloat(String(this.w.seriesData.series[i][j])) + 0.0001) : (stackedNegs[group][j] += parseFloat( String(this.w.seriesData.series[i][j]), )) } } } }) }) Object.entries(stackedPoss).forEach(([key]) => { stackedPoss[key].forEach( (/** @type {any} */ _, /** @type {number} */ stgi) => { gl.maxY = Math.max(gl.maxY, stackedPoss[key][stgi]) gl.minY = Math.min(gl.minY, stackedNegs[key][stgi]) }, ) }) } } export default Range