blob: 5a0abb67d2ea6905c885202c5aef4a29ec4bb059 [file] [log] [blame]
/*
* 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 } 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 = 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.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);
if (min > max) {
min = NaN;
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);
}