| /* | |
| * 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); | |
| } |