echarts scaleRawExtentInfo 源码

  • 2022-10-20
  • 浏览 (445)

echarts scaleRawExtentInfo 代码

文件路径:/src/coord/scaleRawExtentInfo.ts

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { assert, isArray, eqNaN, isFunction } from 'zrender/src/core/util';
import Scale from '../scale/Scale';
import { AxisBaseModel } from './AxisBaseModel';
import { parsePercent } from 'zrender/src/contain/text';
import { AxisBaseOption, CategoryAxisBaseOption } from './axisCommonTypes';
import { ScaleDataValue } from '../util/types';


export interface ScaleRawExtentResult {
    // `min`/`max` defines data available range, determined by
    // `dataMin`/`dataMax` and explicit specified min max related option.
    // The final extent will be based on the `min`/`max` and may be enlarge
    // a little (say, "nice strategy", e.g., niceScale, boundaryGap).
    // Ensure `min`/`max` be finite number or NaN here.
    // (not to be null/undefined) `NaN` means min/max axis is blank.
    readonly min: number;
    readonly max: number;
    // `minFixed`/`maxFixed` marks that `min`/`max` should be used
    // in the final extent without other "nice strategy".
    readonly minFixed: boolean;
    readonly maxFixed: boolean;
    // Mark that the axis should be blank.
    readonly isBlank: boolean;
}

export class ScaleRawExtentInfo {

    private _needCrossZero: boolean;
    private _isOrdinal: boolean;
    private _axisDataLen: number;
    private _boundaryGapInner: number[];

    // Accurate raw value get from model.
    private _modelMinRaw: AxisBaseOption['min'];
    private _modelMaxRaw: AxisBaseOption['max'];

    // Can be `finite number`/`null`/`undefined`/`NaN`
    private _modelMinNum: number;
    private _modelMaxNum: number;

    // Range union by series data on this axis.
    // May be modified if data is filtered.
    private _dataMin: number;
    private _dataMax: number;

    // Highest priority if specified.
    private _determinedMin: number;
    private _determinedMax: number;

    // Make that the `rawExtentInfo` can not be modified any more.
    readonly frozen: boolean;


    constructor(
        scale: Scale,
        model: AxisBaseModel,
        // Usually: data extent from all series on this axis.
        originalExtent: number[]
    ) {
        this._prepareParams(scale, model, originalExtent);
    }

    /**
     * Parameters depending on ouside (like model, user callback)
     * are prepared and fixed here.
     */
    private _prepareParams(
        scale: Scale,
        model: AxisBaseModel,
        // Usually: data extent from all series on this axis.
        dataExtent: number[]
    ) {
        if (dataExtent[1] < dataExtent[0]) {
            dataExtent = [NaN, NaN];
        }
        this._dataMin = dataExtent[0];
        this._dataMax = dataExtent[1];

        const isOrdinal = this._isOrdinal = scale.type === 'ordinal';
        this._needCrossZero = scale.type === 'interval' && model.getNeedCrossZero && model.getNeedCrossZero();

        const modelMinRaw = this._modelMinRaw = model.get('min', true);
        if (isFunction(modelMinRaw)) {
            // This callback alway provide users the full data extent (before data filtered).
            this._modelMinNum = parseAxisModelMinMax(scale, modelMinRaw({
                min: dataExtent[0],
                max: dataExtent[1]
            }));
        }
        else if (modelMinRaw !== 'dataMin') {
            this._modelMinNum = parseAxisModelMinMax(scale, modelMinRaw);
        }

        const modelMaxRaw = this._modelMaxRaw = model.get('max', true);
        if (isFunction(modelMaxRaw)) {
            // This callback alway provide users the full data extent (before data filtered).
            this._modelMaxNum = parseAxisModelMinMax(scale, modelMaxRaw({
                min: dataExtent[0],
                max: dataExtent[1]
            }));
        }
        else if (modelMaxRaw !== 'dataMax') {
            this._modelMaxNum = parseAxisModelMinMax(scale, modelMaxRaw);
        }

        if (isOrdinal) {
            // FIXME: there is a flaw here: if there is no "block" data processor like `dataZoom`,
            // and progressive rendering is using, here the category result might just only contain
            // the processed chunk rather than the entire result.
            this._axisDataLen = model.getCategories().length;
        }
        else {
            const boundaryGap = (model as AxisBaseModel<CategoryAxisBaseOption>).get('boundaryGap');
            const boundaryGapArr = isArray(boundaryGap)
                ? boundaryGap : [boundaryGap || 0, boundaryGap || 0];

            if (typeof boundaryGapArr[0] === 'boolean' || typeof boundaryGapArr[1] === 'boolean') {
                if (__DEV__) {
                    console.warn('Boolean type for boundaryGap is only '
                        + 'allowed for ordinal axis. Please use string in '
                        + 'percentage instead, e.g., "20%". Currently, '
                        + 'boundaryGap is set to be 0.');
                }
                this._boundaryGapInner = [0, 0];
            }
            else {
                this._boundaryGapInner = [
                    parsePercent(boundaryGapArr[0], 1),
                    parsePercent(boundaryGapArr[1], 1)
                ];
            }
        }
    }

    /**
     * Calculate extent by prepared parameters.
     * This method has no external dependency and can be called duplicatedly,
     * getting the same result.
     * If parameters changed, should call this method to recalcuate.
     */
    calculate(): ScaleRawExtentResult {
        // Notice: When min/max is not set (that is, when there are null/undefined,
        // which is the most common case), these cases should be ensured:
        // (1) For 'ordinal', show all axis.data.
        // (2) For others:
        //      + `boundaryGap` is applied (if min/max set, boundaryGap is
        //      disabled).
        //      + If `needCrossZero`, min/max should be zero, otherwise, min/max should
        //      be the result that originalExtent enlarged by boundaryGap.
        // (3) If no data, it should be ensured that `scale.setBlank` is set.

        const isOrdinal = this._isOrdinal;
        const dataMin = this._dataMin;
        const dataMax = this._dataMax;
        const axisDataLen = this._axisDataLen;
        const boundaryGapInner = this._boundaryGapInner;

        const span = !isOrdinal
            ? ((dataMax - dataMin) || Math.abs(dataMin))
            : null;

        // Currently if a `'value'` axis model min is specified as 'dataMin'/'dataMax',
        // `boundaryGap` will not be used. It's the different from specifying as `null`/`undefined`.
        let min = this._modelMinRaw === 'dataMin' ? dataMin : this._modelMinNum;
        let max = this._modelMaxRaw === 'dataMax' ? dataMax : this._modelMaxNum;

        // If `_modelMinNum`/`_modelMaxNum` is `null`/`undefined`, should not be fixed.
        let minFixed = min != null;
        let maxFixed = max != null;

        if (min == null) {
            min = isOrdinal
                ? (axisDataLen ? 0 : NaN)
                : dataMin - boundaryGapInner[0] * span;
        }
        if (max == null) {
            max = isOrdinal
                ? (axisDataLen ? axisDataLen - 1 : NaN)
                : dataMax + boundaryGapInner[1] * span;
        }

        (min == null || !isFinite(min)) && (min = NaN);
        (max == null || !isFinite(max)) && (max = NaN);

        const isBlank = eqNaN(min)
            || eqNaN(max)
            || (isOrdinal && !axisDataLen);

        // If data extent modified, need to recalculated to ensure cross zero.
        if (this._needCrossZero) {
            // Axis is over zero and min is not set
            if (min > 0 && max > 0 && !minFixed) {
                min = 0;
                // minFixed = true;
            }
            // Axis is under zero and max is not set
            if (min < 0 && max < 0 && !maxFixed) {
                max = 0;
                // maxFixed = true;
            }
            // PENDING:
            // When `needCrossZero` and all data is positive/negative, should it be ensured
            // that the results processed by boundaryGap are positive/negative?
            // If so, here `minFixed`/`maxFixed` need to be set.
        }

        const determinedMin = this._determinedMin;
        const determinedMax = this._determinedMax;
        if (determinedMin != null) {
            min = determinedMin;
            minFixed = true;
        }
        if (determinedMax != null) {
            max = determinedMax;
            maxFixed = true;
        }

        // Ensure min/max be finite number or NaN here. (not to be null/undefined)
        // `NaN` means min/max axis is blank.
        return {
            min: min,
            max: max,
            minFixed: minFixed,
            maxFixed: maxFixed,
            isBlank: isBlank
        };
    }

    modifyDataMinMax(minMaxName: 'min' | 'max', val: number): void {
        if (__DEV__) {
            assert(!this.frozen);
        }
        this[DATA_MIN_MAX_ATTR[minMaxName]] = val;
    }

    setDeterminedMinMax(minMaxName: 'min' | 'max', val: number): void {
        const attr = DETERMINED_MIN_MAX_ATTR[minMaxName];
        if (__DEV__) {
            assert(
                !this.frozen
                // Earse them usually means logic flaw.
                && (this[attr] == null)
            );
        }
        this[attr] = val;
    }

    freeze() {
        // @ts-ignore
        this.frozen = true;
    }
}

const DETERMINED_MIN_MAX_ATTR = { min: '_determinedMin', max: '_determinedMax' } as const;
const DATA_MIN_MAX_ATTR = { min: '_dataMin', max: '_dataMax' } as const;

/**
 * Get scale min max and related info only depends on model settings.
 * This method can be called after coordinate system created.
 * For example, in data processing stage.
 *
 * Scale extent info probably be required multiple times during a workflow.
 * For example:
 * (1) `dataZoom` depends it to get the axis extent in "100%" state.
 * (2) `processor/extentCalculator` depends it to make sure whether axis extent is specified.
 * (3) `coordSys.update` use it to finally decide the scale extent.
 * But the callback of `min`/`max` should not be called multiple times.
 * The code below should not be implemented repeatedly either.
 * So we cache the result in the scale instance, which will be recreated at the begining
 * of the workflow (because `scale` instance will be recreated each round of the workflow).
 */
export function ensureScaleRawExtentInfo(
    scale: Scale,
    model: AxisBaseModel,
    // Usually: data extent from all series on this axis.
    originalExtent: number[]
): ScaleRawExtentInfo {

    // Do not permit to recreate.
    let rawExtentInfo = scale.rawExtentInfo;
    if (rawExtentInfo) {
        return rawExtentInfo;
    }

    rawExtentInfo = new ScaleRawExtentInfo(scale, model, originalExtent);
    // @ts-ignore
    scale.rawExtentInfo = rawExtentInfo;

    return rawExtentInfo;
}

export function parseAxisModelMinMax(scale: Scale, minMax: ScaleDataValue): number {
    return minMax == null ? null
        : eqNaN(minMax) ? NaN
        : scale.parse(minMax);
}

相关信息

echarts 源码目录

相关文章

echarts Axis 源码

echarts AxisBaseModel 源码

echarts CoordinateSystem 源码

echarts View 源码

echarts axisAlignTicks 源码

echarts axisCommonTypes 源码

echarts axisDefault 源码

echarts axisHelper 源码

echarts axisModelCommonMixin 源码

echarts axisModelCreator 源码

0  赞