estimateTicks.mjs

import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import range from 'lodash-es/range.js'
import first from 'lodash-es/first.js'
import last from 'lodash-es/last.js'
import sortBy from 'lodash-es/sortBy.js'
import filter from 'lodash-es/filter.js'
import isnum from './isnum.mjs'
import isearr from './isearr.mjs'
import isint from './isint.mjs'
import cdbl from './cdbl.mjs'
import round from './round.mjs'
import ceil from './ceil.mjs'
import preciseNum from './preciseNum.mjs'


let norm = (v) => {
    let b = 1
    if (v < 0) {
        b = -1
    }
    v = Math.abs(v)
    let rr = null
    if (v < 0.1) {
        for (let i = 1; i < 16; i++) {
            let r = Math.pow(10, i)
            v *= r
            if (v < 1) {
                rr = {
                    r: 1 / r, //乘值恢復v
                    v: b * v,
                }
                break
            }
        }
    }
    else if (v < 1) {
        return {
            r: 1, //乘值恢復v
            v: b * v,
        }
    }
    else if (v >= 1) {
        for (let i = 1; i < 16; i++) {
            let r = Math.pow(10, i)
            v /= r
            if (v < 1) {
                rr = {
                    r, //乘值恢復v
                    v: b * v,
                }
                break
            }
        }
    }

    if (rr === null) {
        throw new Error(`can not normalize v[${v}]`)
    }
    return rr
}

/**
 * 給予最小與最大值,估計適合的繪圖刻度
 *
 * Unit Test: {@link https://github.com/yuda-lyu/./blob/master/test/estimateTicks.test.mjs Github}
 * @memberOf wsemi
 * @param {Number|String} rmin 輸入數字或字串
 * @param {Number|String} rmax 輸入數字或字串
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Array} [opt.testTickNums=[3, 4, 5]] 輸入可供測試刻度數量陣列,預設[3, 4, 5]
 * @returns {Object} 回傳繪圖刻度物件,包含刻度數量tickNum、刻度間距tickInterval、刻度位置tickPositions
 * @example
 *
 * let rmin = null
 * let rmax = null
 * let r = null
 *
 * // -4.66~-3.11
 * rmin = -4.66
 * rmax = -3.11
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin -4.66 rmax -3.11 r { tickNum: 3, tickInterval: 0.8, tickPositions: [ -4.7, -3.9, -3.1 ], tickDig: 1 }
 *
 * // 0~0.9
 * rmin = 0
 * rmax = 0.9
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0 rmax 0.9 r { tickNum: 3, tickInterval: 0.5, tickPositions: [ 0, 0.5, 1 ], tickDig: 1 }
 *
 * // 0~1
 * rmin = 0
 * rmax = 1
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0 rmax 1 r { tickNum: 3, tickInterval: 0.5, tickPositions: [ 0, 0.5, 1 ], tickDig: 1 }
 *
 * // 0~99
 * rmin = 0
 * rmax = 99
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0 rmax 99 r { tickNum: 3, tickInterval: 50, tickPositions: [ 0, 50, 100 ], tickDig: 0 }
 *
 * // 0~100
 * rmin = 0
 * rmax = 100
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0 rmax 100 r { tickNum: 3, tickInterval: 50, tickPositions: [ 0, 50, 100 ], tickDig: 0 }
 *
 * // 0.1~0.9
 * rmin = 0.1
 * rmax = 0.9
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.1 rmax 0.9 r { tickNum: 3, tickInterval: 0.4, tickPositions: [ 0.1, 0.5, 0.9 ], tickDig: 1 }
 *
 * // 0.1~1
 * rmin = 0.1
 * rmax = 1
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.1 rmax 1 r { tickNum: 3, tickInterval: 0.5, tickPositions: [ 0, 0.5, 1 ], tickDig: 1 }
 *
 * // 0.1~99
 * rmin = 0.1
 * rmax = 99
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.1 rmax 99 r { tickNum: 3, tickInterval: 50, tickPositions: [ 0, 50, 100 ], tickDig: 0 }
 *
 * // 0.1~100
 * rmin = 0.1
 * rmax = 100
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.1 rmax 100 r { tickNum: 3, tickInterval: 50, tickPositions: [ 0, 50, 100 ], tickDig: 0 }
 *
 * // 0.1~100.1
 * rmin = 0.1
 * rmax = 100.1
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.1 rmax 100.1 r { tickNum: 4, tickInterval: 34, tickPositions: [ 0, 34, 68, 102 ], tickDig: 0 }
 *
 * // 0.89~0.9
 * rmin = 0.89
 * rmax = 0.9
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.89 rmax 0.9 r { tickNum: 3, tickInterval: 0.01, tickPositions: [ 0.88, 0.89, 0.9 ], tickDig: 1 }
 *
 * // 0.89~1
 * rmin = 0.89
 * rmax = 1
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.89 rmax 1 r { tickNum: 3, tickInterval: 0.1, tickPositions: [ 0.8, 0.9, 1 ], tickDig: 1 }
 *
 * // 0.89~99
 * rmin = 0.89
 * rmax = 99
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.89 rmax 99 r { tickNum: 3, tickInterval: 50, tickPositions: [ 0, 50, 100 ], tickDig: 0 }
 *
 * // 0.89~100
 * rmin = 0.89
 * rmax = 100
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.89 rmax 100 r { tickNum: 3, tickInterval: 50, tickPositions: [ 0, 50, 100 ], tickDig: 0 }
 *
 * // 0.89~100.89
 * rmin = 0.89
 * rmax = 100.89
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 0.89 rmax 100.89 r { tickNum: 4, tickInterval: 34, tickPositions: [ 0, 34, 68, 102 ], tickDig: 0 }
 *
 * // 50.89~99
 * rmin = 50.89
 * rmax = 99
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 50.89 rmax 99 r { tickNum: 3, tickInterval: 25, tickPositions: [ 50, 75, 100 ], tickDig: 0 }
 *
 * // 50.89~100
 * rmin = 50.89
 * rmax = 100
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 50.89 rmax 100 r { tickNum: 3, tickInterval: 25, tickPositions: [ 50, 75, 100 ], tickDig: 0 }
 *
 * // 90.89~99
 * rmin = 90.89
 * rmax = 99
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 90.89 rmax 99 r { tickNum: 4, tickInterval: 3, tickPositions: [ 90, 93, 96, 99 ], tickDig: 0 }
 *
 * // 90.89~100
 * rmin = 90.89
 * rmax = 100
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 90.89 rmax 100 r { tickNum: 3, tickInterval: 5, tickPositions: [ 90, 95, 100 ], tickDig: 0 }
 *
 * // 98.9~99
 * rmin = 98.9
 * rmax = 99
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 98.9 rmax 99 r { tickNum: 3, tickInterval: 1, tickPositions: [ 98, 99, 100 ], tickDig: 0 }
 *
 * // 98.9~100
 * rmin = 98.9
 * rmax = 100
 * r = estimateTicks(rmin, rmax)
 * console.log('rmin', rmin, 'rmax', rmax, 'r', r)
 * // => rmin 98.9 rmax 100 r { tickNum: 3, tickInterval: 1, tickPositions: [ 98, 99, 100 ], tickDig: 0 }
 *
 */
function estimateTicks(rmin, rmax, opt = {}) {

    //check rmin
    if (!isnum(rmin)) {
        throw new Error(`rmin is not a number`)
    }
    rmin = cdbl(rmin)

    //check rmax
    if (!isnum(rmax)) {
        throw new Error(`rmax is not a number`)
    }
    rmax = cdbl(rmax)

    //check rmin=rmax
    if (rmin === rmax) {
        let tickInterval = null
        if (rmin === 0) {
            tickInterval = 1
        }
        else {
            tickInterval = Math.abs(rmin / 10)
        }
        let tickDig = preciseNum(tickInterval, { returnDigit: true })
        // console.log('tickDig', tickDig)
        tickInterval = round(tickInterval, tickDig)
        return {
            tickNum: 3,
            tickInterval,
            tickPositions: [rmin - tickInterval, rmin, rmin + tickInterval],
            tickDig,
        }
    }

    //check rmin>rmax
    if (rmin > rmax) {
        throw new Error(`rmin[${rmin}] > rmax[${rmax}]`)
    }

    //testTickNums
    let testTickNums = get(opt, 'testTickNums')
    if (!isearr(testTickNums)) {
        testTickNums = [3, 4, 5]
    }

    let vmin = rmin
    let vmax = rmax
    // console.log('vmin', vmin)
    // console.log('vmax', vmax)

    let nmMin = norm(vmin)
    let nmMax = norm(vmax)
    let rat = Math.max(nmMin.r, nmMax.r)
    let irat = Math.log10(rat)
    // console.log('rat', rat, 'irat', irat, 'nmMin.r', nmMin.r, 'nmMax.r', nmMax.r)

    let yMin = vmin / rat
    let yMax = vmax / rat
    // console.log('yMin', yMin)
    // console.log('yMax', yMax)

    let yCenter = (yMin + yMax) / 2
    // console.log('yCenter', yCenter)

    let yRng = yMax - yMin
    // console.log('yRng', yRng)

    let tks = []
    each(testTickNums, (tickNum) => {
        // console.log('tickNum', tickNum)

        let yIntrv = yRng / (tickNum - 1)
        // console.log('yIntrv', yIntrv)
        // let yIntrvStr = preciseNum(yIntrv)
        // console.log('yIntrvStr', yIntrvStr)
        let yIntrvIdg = preciseNum(yIntrv, { returnDigit: true })
        // console.log('yIntrvIdg', yIntrvIdg)

        //限制yIntrvIdg, 因數據已正規化, 只選擇小數位2位以內去測試
        yIntrvIdg = Math.min(yIntrvIdg, 2)

        //testIdgs
        let testIdgs = range(yIntrvIdg, -1, -1)
        // console.log('testIdgs', testIdgs)

        each(testIdgs, (idg) => {
            // console.log('idg', idg)

            //idgTrue
            let idgTrue = idg - irat
            idgTrue = Math.max(idgTrue, 0)
            // console.log('idgTrue', idgTrue)

            //間距取指定位數之大值
            let testTickInterval = ceil(yIntrv, idg)
            // console.log('testTickInterval', testTickInterval)

            //testTickIntervalDown
            let testTickIntervalDown = testTickInterval - 1 / Math.pow(10, idg)
            testTickIntervalDown = round(testTickIntervalDown, idg)
            // console.log('testTickIntervalDown', testTickIntervalDown)

            //testTickIntervalUp
            let testTickIntervalUp = testTickInterval + 1 / Math.pow(10, idg)
            testTickIntervalUp = round(testTickIntervalUp, idg)
            // console.log('testTickIntervalUp', testTickIntervalUp)

            //testTickIntervals, 添加上下同小數位數之間距
            let testTickIntervals = [
                testTickIntervalDown,
                testTickInterval,
                testTickIntervalUp,
            ]
            testTickIntervals = filter(testTickIntervals, (v) => {
                return v > 0
            })
            // console.log('testTickIntervals', testTickIntervals)

            //重新指定數據中點
            let testCenter = round(yCenter, idg)
            // console.log('testCenter', testCenter)

            //testCenterDown
            let testCenterDown = testCenter - 1 / Math.pow(10, idg)
            testCenterDown = round(testCenterDown, idg)
            // console.log('testCenterDown', testCenterDown)

            //testCenterUp
            let testCenterUp = testCenter + 1 / Math.pow(10, idg)
            testCenterUp = round(testCenterUp, idg)
            // console.log('testCenterUp', testCenterUp)

            //testCenters, 添加上下同小數位數之數據中點
            let testCenters = [
                testCenterDown,
                testCenter,
                testCenterUp,
            ]
            // console.log('testCenters', testCenters)

            each(testTickIntervals, (tickInterval) => {

                //tickIntervalTrue
                let tickIntervalTrue = rat * tickInterval
                tickIntervalTrue = round(tickIntervalTrue, idgTrue)
                // console.log('tickIntervalTrue', tickIntervalTrue)

                each(testCenters, (rngCenter) => {

                    //tickPositions, tickPositionsTrue
                    let tickPositions = []
                    let tickPositionsTrue = []
                    if (true) {

                        let tickNumHalf = (tickNum - 1) / 2
                        let tMin = rngCenter - tickNumHalf * tickInterval
                        for (let i = 0; i < tickNum; i++) {
                            let v = tMin + i * tickInterval
                            v = round(v, idg)
                            tickPositions.push(v)
                            let vTrue = rat * v
                            vTrue = round(vTrue, idgTrue)
                            tickPositionsTrue.push(vTrue)
                        }
                    }
                    // console.log('tickPositions', tickPositions)

                    //minTick
                    let minTick = first(tickPositions)
                    // console.log('minTick', minTick)

                    //maxTick
                    let maxTick = last(tickPositions)
                    // console.log('maxTick', maxTick)

                    //check
                    if (minTick > yMin || maxTick < yMax) {
                        return true //跳出換下一個
                    }

                    //overMin
                    let overMin = (yMin - minTick) / yRng
                    // console.log('overMin', overMin)

                    //overMax
                    let overMax = (maxTick - yMax) / yRng
                    // console.log('overMax', overMax)

                    //rd
                    let rd = (overMin + overMax) / 2
                    // console.log('rd', rd)

                    //numInt, 真實刻度值為整數時, 降低權重
                    let numInt = 0
                    each(tickPositionsTrue, (v) => {
                        if (isint(v)) {
                            numInt++
                        }
                    })
                    let ratInt = numInt / tickNum
                    // console.log('numInt', numInt)
                    // console.log('ratInt', ratInt)

                    //num0, 正規化刻度值為0時, 降低權重
                    let num0 = 0
                    each(tickPositions, (v) => {
                        if (v === 0) {
                            num0++
                        }
                    })
                    let rat0 = num0 / tickNum
                    // console.log('num0', num0)
                    // console.log('rat0', rat0)

                    //num5, 正規化刻度值為5,50,500...等序列時, 降低權重
                    let num5 = 0
                    each(tickPositions, (v) => {
                        if (v === 0.5) {
                            num5++
                        }
                    })
                    let rat5 = num5 / tickNum
                    // console.log('num5', num5)
                    // console.log('rat5', rat5)

                    //num10, 正規化刻度值為10,100,1000...等序列時, 降低權重
                    let num10 = 0
                    each(tickPositions, (v) => {
                        if (v === 1) {
                            num10++
                        }
                    })
                    let rat10 = num10 / tickNum
                    // console.log('num10', num10)
                    // console.log('rat10', rat10)

                    //fitness, 適應函數值
                    let fitness = rd + (idg * 0.2) + (tickNum * 0.1) + (overMin * 1.5) + (overMax * 1.5) + (1 - ratInt * 0.25) + (1 - rat0 * 0.4) + (1 - rat5 * 0.3) + (1 - rat10 * 0.3)

                    //tk
                    let tk = {
                        diff: rd,
                        rngCenter,
                        numInt,
                        ratInt,
                        num0,
                        rat0,
                        num5,
                        rat5,
                        num10,
                        rat10,
                        overMin,
                        overMax,
                        fitness,
                        idg,
                        tickNum,
                        _tickDig: idg,
                        // __tickInterval: yIntrv,
                        _tickInterval: tickInterval,
                        _tickPositions: tickPositions,
                        tickDig: idgTrue,
                        tickInterval: tickIntervalTrue,
                        tickPositions: tickPositionsTrue,
                    }
                    // console.log('tk', tk)

                    //push
                    tks.push(tk)

                })

            })

        })

    })
    // console.log('tks', tks)

    //sortBy
    tks = sortBy(tks, 'fitness')
    // console.log('tks(sortBy)', tks)

    //tk
    let tk = get(tks, 0)
    // console.log('tk', tk)

    return {
        tickNum: tk.tickNum,
        tickInterval: tk.tickInterval,
        tickPositions: tk.tickPositions,
        tickDig: tk.tickDig,
    }
}


export default estimateTicks