// @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 (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
          }
        }
      }
      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
          }

          // 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 !== '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)
              },
            )
          }
          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 === '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 = () => {
      for (let i = 0; i < this.w.seriesData.series.length; i++) {
        if (this.w.labelData.labels[i]) {
          for (let j = 0; j < this.w.labelData.labels[i].length; j++) {
            if (
              this.w.labelData.labels[i][j] !== null &&
              Utils.isNumber(this.w.labelData.labels[i][j])
            ) {
              gl.maxX = Math.max(
                gl.maxX,
                /** @type {number} */ (
                  /** @type {any} */ (this.w.labelData.labels[i][j])
                ),
              )
              gl.initialMaxX = Math.max(
                gl.maxX,
                /** @type {number} */ (
                  /** @type {any} */ (this.w.labelData.labels[i][j])
                ),
              )
              gl.minX = Math.min(
                gl.minX,
                /** @type {number} */ (
                  /** @type {any} */ (this.w.labelData.labels[i][j])
                ),
              )
              gl.initialMinX = Math.min(
                gl.minX,
                /** @type {number} */ (
                  /** @type {any} */ (this.w.labelData.labels[i][j])
                ),
              )
            }
          }
        }
      }
    }
    // 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
       */
      this.w.seriesData.seriesX.forEach((sX) => {
        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
              ],
            )
          }

          // 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
