|  | /* | 
|  | * 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 outside (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 always provides users the full data extent (before data is 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 always provides users the full data extent (before data is 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 beginning | 
|  | * 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); | 
|  | } |