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