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