You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2020/06/19 09:36:55 UTC

[incubator-echarts] 01/02: fix: some refactor about dataZoom and axis scale extent calculation:

This is an automated email from the ASF dual-hosted git repository.

sushuang pushed a commit to branch extent_filtered_by_other_axis
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git

commit b116faa8ad5db4a887d4b6c0f9a23ec52534f8f2
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Jun 19 17:33:02 2020 +0800

    fix: some refactor about dataZoom and axis scale extent calculation:
    
    (1) The "scale extent calculation" is depended by both coord sys and "dataZoom".
    So it need to be performed in different stage of workflow and should avoid to call it repleatly or implement it repeatly.
    But previously, the "scale extent calculation" is implemented both in `axisHelper.ts` and `dataZoom/AxisProxy.ts`,
    where different code implements similar logic.
    "Scale extent calculation" is based on input of:
        (I) option specified min/max/scale (including callback) and
        (II) dataExtent and
        (III) some filter related components like dataZoom.
    Currently we need add more filters depends on that axis scale calculation.
    e.g.,
        (I) one axis min max effect the other axis extent
        (II) custom filter.
    So we refactor that "Scale extent calculation" as a reusable module (see `scaleRawExtentInfo.ts`).
    
    (2) Based on (1), make the callback of axis.min/axis.max consistent whatever it is called in
    "data processing stage" or "coord sys updating stage"
    
    (3) Based on (1), fix some "inconsistent" flaw in dataZoom when handling stacked series.
    
    (4) Fix the type of axis.min/max to `ScaleDataValue`. And enable the callback of axis.min/max to return `ScaleDataValue`.
    
    (5) Remove some code that never used.
    
    (6) Make the "data filter", which will "block stream", not as the default data processor in the register API.
---
 src/component/dataZoom/AxisProxy.ts         | 140 ++++--------
 src/component/dataZoom/DataZoomModel.ts     |   4 +-
 src/component/dataZoom/dataZoomProcessor.ts |   2 +-
 src/component/dataZoom/helper.ts            |   1 +
 src/component/gridSimple.ts                 |   1 +
 src/coord/axisCommonTypes.ts                |  16 +-
 src/coord/axisHelper.ts                     | 163 +++++---------
 src/coord/axisModelCommonMixin.ts           |  57 +----
 src/coord/cartesian/AxisModel.ts            |  15 --
 src/coord/cartesian/Grid.ts                 |  55 ++---
 src/coord/cartesian/cartesianAxisHelper.ts  |  35 +++
 src/coord/polar/polarCreator.ts             |  16 +-
 src/coord/radar/Radar.ts                    |   7 +-
 src/coord/scaleRawExtentInfo.ts             | 316 ++++++++++++++++++++++++++++
 src/data/List.ts                            |  16 +-
 src/echarts.ts                              |   7 +-
 src/helper.ts                               |   2 -
 src/scale/Scale.ts                          |   5 +
 18 files changed, 513 insertions(+), 345 deletions(-)

diff --git a/src/component/dataZoom/AxisProxy.ts b/src/component/dataZoom/AxisProxy.ts
index 8e9a7aa..95a572e 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -26,9 +26,11 @@ import SeriesModel from '../../model/Series';
 import ExtensionAPI from '../../ExtensionAPI';
 import { Dictionary } from '../../util/types';
 // TODO Polar?
-import CartesianAxisModel from '../../coord/cartesian/AxisModel';
+// import CartesianAxisModel from '../../coord/cartesian/AxisModel';
 import DataZoomModel from './DataZoomModel';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
+import { unionAxisExtentFromData } from '../../coord/axisHelper';
+import { ensureScaleRawExtentInfo } from '../../coord/scaleRawExtentInfo';
 
 const each = zrUtil.each;
 const asc = numberUtil.asc;
@@ -127,31 +129,31 @@ class AxisProxy {
         return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex) as AxisBaseModel;
     }
 
-    getOtherAxisModel() {
-        const axisDim = this._dimName;
-        const ecModel = this.ecModel;
-        const axisModel = this.getAxisModel();
-        const isCartesian = axisDim === 'x' || axisDim === 'y';
-        let otherAxisDim: 'x' | 'y' | 'radius' | 'angle';
-        let coordSysIndexName: 'gridIndex' | 'polarIndex';
-        if (isCartesian) {
-            coordSysIndexName = 'gridIndex';
-            otherAxisDim = axisDim === 'x' ? 'y' : 'x';
-        }
-        else {
-            coordSysIndexName = 'polarIndex';
-            otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle';
-        }
-        let foundOtherAxisModel;
-        ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) {
-            if (((otherAxisModel as CartesianAxisModel).get(coordSysIndexName as 'gridIndex') || 0)
-                === ((axisModel as CartesianAxisModel).get(coordSysIndexName as 'gridIndex') || 0)
-            ) {
-                foundOtherAxisModel = otherAxisModel;
-            }
-        });
-        return foundOtherAxisModel;
-    }
+    // getOtherAxisModel() {
+    //     const axisDim = this._dimName;
+    //     const ecModel = this.ecModel;
+    //     const axisModel = this.getAxisModel();
+    //     const isCartesian = axisDim === 'x' || axisDim === 'y';
+    //     let otherAxisDim: 'x' | 'y' | 'radius' | 'angle';
+    //     let coordSysIndexName: 'gridIndex' | 'polarIndex';
+    //     if (isCartesian) {
+    //         coordSysIndexName = 'gridIndex';
+    //         otherAxisDim = axisDim === 'x' ? 'y' : 'x';
+    //     }
+    //     else {
+    //         coordSysIndexName = 'polarIndex';
+    //         otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle';
+    //     }
+    //     let foundOtherAxisModel;
+    //     ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) {
+    //         if (((otherAxisModel as CartesianAxisModel).get(coordSysIndexName as 'gridIndex') || 0)
+    //             === ((axisModel as CartesianAxisModel).get(coordSysIndexName as 'gridIndex') || 0)
+    //         ) {
+    //             foundOtherAxisModel = otherAxisModel;
+    //         }
+    //     });
+    //     return foundOtherAxisModel;
+    // }
 
     getMinMaxSpan() {
         return zrUtil.clone(this._minMaxSpan);
@@ -291,15 +293,6 @@ class AxisProxy {
         this._setAxisModel();
     }
 
-    restore(dataZoomModel: DataZoomModel) {
-        if (dataZoomModel !== this._dataZoomModel) {
-            return;
-        }
-
-        this._valueWindow = this._percentWindow = null;
-        this._setAxisModel(true);
-    }
-
     filterData(dataZoomModel: DataZoomModel, api: ExtensionAPI) {
         if (dataZoomModel !== this._dataZoomModel) {
             return;
@@ -421,7 +414,7 @@ class AxisProxy {
         }, this);
     }
 
-    private _setAxisModel(isRestore?: boolean) {
+    private _setAxisModel() {
 
         const axisModel = this.getAxisModel();
 
@@ -435,34 +428,29 @@ class AxisProxy {
         // [0, 500]: arbitrary value, guess axis extent.
         let precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
         precision = Math.min(precision, 20);
-        // isRestore or isFull
-        const useOrigin = isRestore || (percentWindow[0] === 0 && percentWindow[1] === 100);
 
-        axisModel.setRange(
-            useOrigin ? null : +valueWindow[0].toFixed(precision),
-            useOrigin ? null : +valueWindow[1].toFixed(precision)
-        );
+        // For value axis, if min/max/scale are not set, we just use the extent obtained
+        // by series data, which may be a little different from the extent calculated by
+        // `axisHelper.getScaleExtent`. But the different just affects the experience a
+        // little when zooming. So it will not be fixed until some users require it strongly.
+        const rawExtentInfo = axisModel.axis.scale.rawExtentInfo;
+        if (percentWindow[0] !== 0) {
+            rawExtentInfo.setDeterminedMinMax('min', +valueWindow[0].toFixed(precision));
+        }
+        if (percentWindow[1] !== 100) {
+            rawExtentInfo.setDeterminedMinMax('max', +valueWindow[1].toFixed(precision));
+        }
+        rawExtentInfo.freeze();
     }
 }
 
 function calculateDataExtent(axisProxy: AxisProxy, axisDim: string, seriesModels: SeriesModel[]) {
-    let dataExtent = [Infinity, -Infinity];
+    const dataExtent = [Infinity, -Infinity];
 
     each(seriesModels, function (seriesModel) {
-        const seriesData = seriesModel.getData();
-        if (seriesData) {
-            each(seriesData.mapDimensionsAll(axisDim), function (dim) {
-                const seriesExtent = seriesData.getApproximateExtent(dim);
-                seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]);
-                seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]);
-            });
-        }
+        unionAxisExtentFromData(dataExtent, seriesModel.getData(), axisDim);
     });
 
-    if (dataExtent[1] < dataExtent[0]) {
-        dataExtent = [NaN, NaN];
-    }
-
     // It is important to get "consistent" extent when more then one axes is
     // controlled by a `dataZoom`, otherwise those axes will not be synchronized
     // when zooming. But it is difficult to know what is "consistent", considering
@@ -472,46 +460,10 @@ function calculateDataExtent(axisProxy: AxisProxy, axisDim: string, seriesModels
     // extent can be obtained from axis.data).
     // Nevertheless, user can set min/max/scale on axes to make extent of axes
     // consistent.
-    fixExtentByAxis(axisProxy, dataExtent);
-
-    return dataExtent as [number, number];
-}
-
-function fixExtentByAxis(axisProxy: AxisProxy, dataExtent: number[]) {
-    const axisModel = axisProxy.getAxisModel() as CartesianAxisModel;
-    const min = axisModel.getMin(true);
-
-    // For category axis, if min/max/scale are not set, extent is determined
-    // by axis.data by default.
-    const isCategoryAxis = axisModel.get('type') === 'category';
-    const axisDataLen = isCategoryAxis && axisModel.getCategories().length;
-
-    if (min != null && min !== 'dataMin' && typeof min !== 'function') {
-        dataExtent[0] = min;
-    }
-    else if (isCategoryAxis) {
-        dataExtent[0] = axisDataLen > 0 ? 0 : NaN;
-    }
-
-    const max = axisModel.getMax(true);
-    if (max != null && max !== 'dataMax' && typeof max !== 'function') {
-        dataExtent[1] = max;
-    }
-    else if (isCategoryAxis) {
-        dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN;
-    }
-
-    if (!axisModel.get('scale', true)) {
-        dataExtent[0] > 0 && (dataExtent[0] = 0);
-        dataExtent[1] < 0 && (dataExtent[1] = 0);
-    }
-
-    // For value axis, if min/max/scale are not set, we just use the extent obtained
-    // by series data, which may be a little different from the extent calculated by
-    // `axisHelper.getScaleExtent`. But the different just affects the experience a
-    // little when zooming. So it will not be fixed until some users require it strongly.
+    const axisModel = axisProxy.getAxisModel();
+    const rawExtentResult = ensureScaleRawExtentInfo(axisModel.axis.scale, axisModel, dataExtent).calculate();
 
-    return dataExtent;
+    return [rawExtentResult.min, rawExtentResult.max] as [number, number];
 }
 
 export default AxisProxy;
diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index fad975c..f711444 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -268,10 +268,10 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
 
         this._resetTarget();
 
-        this._giveAxisProxies();
+        this._prepareAxisProxies();
     }
 
-    private _giveAxisProxies() {
+    private _prepareAxisProxies() {
         const axisProxies = this._axisProxies;
 
         this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) {
diff --git a/src/component/dataZoom/dataZoomProcessor.ts b/src/component/dataZoom/dataZoomProcessor.ts
index 3a255f5..960d62c 100644
--- a/src/component/dataZoom/dataZoomProcessor.ts
+++ b/src/component/dataZoom/dataZoomProcessor.ts
@@ -22,7 +22,7 @@ import {createHashMap, each} from 'zrender/src/core/util';
 import SeriesModel from '../../model/Series';
 import DataZoomModel from './DataZoomModel';
 
-echarts.registerProcessor({
+echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, {
 
     // `dataZoomProcessor` will only be performed in needed series. Consider if
     // there is a line series and a pie series, it is better not to update the
diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts
index 4860f27..ef364b1 100644
--- a/src/component/dataZoom/helper.ts
+++ b/src/component/dataZoom/helper.ts
@@ -32,6 +32,7 @@ export interface DataZoomPayloadBatchItem {
 
 const AXIS_DIMS = ['x', 'y', 'z', 'radius', 'angle', 'single'] as const;
 // Supported coords.
+// FIXME: polar has been broken (but rarely used).
 const COORDS = ['cartesian2d', 'polar', 'singleAxis'] as const;
 
 export function isCoordSupported(coordType: string) {
diff --git a/src/component/gridSimple.ts b/src/component/gridSimple.ts
index a46b050..a73ab83 100644
--- a/src/component/gridSimple.ts
+++ b/src/component/gridSimple.ts
@@ -22,6 +22,7 @@ import * as zrUtil from 'zrender/src/core/util';
 import * as graphic from '../util/graphic';
 
 import './axis';
+// import '../coord/cartesian/defaultAxisExtentFromData';
 import ComponentView from '../view/Component';
 import GlobalModel from '../model/Global';
 import GridModel from '../coord/cartesian/GridModel';
diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts
index 4dac225..fb00c4b 100644
--- a/src/coord/axisCommonTypes.ts
+++ b/src/coord/axisCommonTypes.ts
@@ -20,7 +20,7 @@
 import {
     TextCommonOption, LineStyleOption, OrdinalRawValue, ZRColor,
     AreaStyleOption, ComponentOption, ColorString,
-    AnimationOptionMixin, Dictionary
+    AnimationOptionMixin, Dictionary, ScaleDataValue
 } from '../util/types';
 
 
@@ -71,21 +71,15 @@ export interface AxisBaseOption extends ComponentOption,
     boundaryGap?: boolean | [number | string, number | string];
 
     // Min value of the axis. can be:
-    // + a number
+    // + ScaleDataValue
     // + 'dataMin': use the min value in data.
     // + null/undefined: auto decide min value (consider pretty look and boundaryGap).
-    min?: number | 'dataMin' | ((extent: {min: number, max: number}) => number);
+    min?: ScaleDataValue | 'dataMin' | ((extent: {min: number, max: number}) => ScaleDataValue);
     // Max value of the axis. can be:
-    // + a number
+    // + ScaleDataValue
     // + 'dataMax': use the max value in data.
     // + null/undefined: auto decide max value (consider pretty look and boundaryGap).
-    max?: number | 'dataMax' | ((extent: {min: number, max: number}) => number);
-    // Readonly prop, specifies start value of the range when using data zoom.
-    // Only for internal usage.
-    rangeStart?: number;
-    // Readonly prop, specifies end value of the range when using data zoom.
-    // Only for internal usage.
-    rangeEnd?: number;
+    max?: ScaleDataValue | 'dataMax' | ((extent: {min: number, max: number}) => ScaleDataValue);
     // Optional value can be:
     // + `false`: always include value 0.
     // + `true`: the extent do not consider value 0.
diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts
index 9eeac8f..ea3efdb 100644
--- a/src/coord/axisHelper.ts
+++ b/src/coord/axisHelper.ts
@@ -1,6 +1,6 @@
 /*
 * Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
+* or more contributor license agreements.  See theICE 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
@@ -17,12 +17,10 @@
 * under the License.
 */
 
-import {__DEV__} from '../config';
 import * as zrUtil from 'zrender/src/core/util';
 import OrdinalScale from '../scale/Ordinal';
 import IntervalScale from '../scale/Interval';
 import Scale from '../scale/Scale';
-import * as numberUtil from '../util/number';
 import {
     prepareLayoutBarSeries,
     makeColumnLayout,
@@ -37,119 +35,31 @@ import LogScale from '../scale/Log';
 import Axis from './Axis';
 import { AxisBaseOption } from './axisCommonTypes';
 import type CartesianAxisModel from './cartesian/AxisModel';
+import List from '../data/List';
+import { getStackedDimension } from '../data/helper/dataStackHelper';
+import { Dictionary, ScaleDataValue } from '../util/types';
+import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo';
+
 
 type BarWidthAndOffset = ReturnType<typeof makeColumnLayout>;
 
 /**
  * Get axis scale extent before niced.
  * Item of returned array can only be number (including Infinity and NaN).
+ *
+ * Caution:
+ * Precondition of calling this method:
+ * The scale extent has been initailized with data extent from data via
+ * `scale.setExtent` or `scale.unionExtentFromData`;
  */
 export function getScaleExtent(scale: Scale, model: AxisBaseModel) {
     const scaleType = scale.type;
+    const rawExtentResult = ensureScaleRawExtentInfo(scale, model, scale.getExtent()).calculate();
 
-    let min = model.getMin();
-    let max = model.getMax();
-    const originalExtent = scale.getExtent();
-
-    let axisDataLen;
-    let boundaryGapInner: number[];
-    let span;
-    if (scaleType === 'ordinal') {
-        axisDataLen = model.getCategories().length;
-    }
-    else {
-        const boundaryGap = model.get('boundaryGap');
-        const boundaryGapArr = zrUtil.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.');
-            }
-            boundaryGapInner = [0, 0];
-        }
-        else {
-            boundaryGapInner = [
-                numberUtil.parsePercent(boundaryGapArr[0], 1),
-                numberUtil.parsePercent(boundaryGapArr[1], 1)
-            ];
-        }
-        span = (originalExtent[1] - originalExtent[0])
-            || Math.abs(originalExtent[0]);
-    }
-
-    // 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.
-
-    // FIXME
-    // (1) When min/max is 'dataMin' or 'dataMax', should boundaryGap be able to used?
-    // (2) When `needCrossZero` and all data is positive/negative, should it be ensured
-    // that the results processed by boundaryGap are positive/negative?
-
-    if (min === 'dataMin') {
-        min = originalExtent[0];
-    }
-    else if (typeof min === 'function') {
-        min = min({
-            min: originalExtent[0],
-            max: originalExtent[1]
-        });
-    }
-
-    if (max === 'dataMax') {
-        max = originalExtent[1];
-    }
-    else if (typeof max === 'function') {
-        max = max({
-            min: originalExtent[0],
-            max: originalExtent[1]
-        });
-    }
+    scale.setBlank(rawExtentResult.isBlank);
 
-    const fixMin = min != null;
-    const fixMax = max != null;
-
-    if (min == null) {
-        min = scaleType === 'ordinal'
-            ? (axisDataLen ? 0 : NaN)
-            : originalExtent[0] - boundaryGapInner[0] * span;
-    }
-    if (max == null) {
-        max = scaleType === 'ordinal'
-            ? (axisDataLen ? axisDataLen - 1 : NaN)
-            : originalExtent[1] + boundaryGapInner[1] * span;
-    }
-
-    (min == null || !isFinite(min)) && (min = NaN);
-    (max == null || !isFinite(max)) && (max = NaN);
-
-    scale.setBlank(
-        zrUtil.eqNaN(min)
-        || zrUtil.eqNaN(max)
-        || ((scale instanceof OrdinalScale) && !scale.getOrdinalMeta().categories.length)
-    );
-
-    // Evaluate if axis needs cross zero
-    if (model.getNeedCrossZero()) {
-        // Axis is over zero and min is not set
-        if (min > 0 && max > 0 && !fixMin) {
-            min = 0;
-        }
-        // Axis is under zero and max is not set
-        if (min < 0 && max < 0 && !fixMax) {
-            max = 0;
-        }
-    }
+    let min = rawExtentResult.min;
+    let max = rawExtentResult.max;
 
     // If bars are placed on a base axis of type time or interval account for axis boundary overflow and current axis
     // is base axis
@@ -185,8 +95,8 @@ export function getScaleExtent(scale: Scale, model: AxisBaseModel) {
         extent: [min, max],
         // "fix" means "fixed", the value should not be
         // changed in the subsequent steps.
-        fixMin: fixMin,
-        fixMax: fixMax
+        fixMin: rawExtentResult.minFixed,
+        fixMax: rawExtentResult.maxFixed
     };
 }
 
@@ -230,6 +140,9 @@ function adjustScaleForOverflow(
     return {min: min, max: max};
 }
 
+// Precondition of calling this method:
+// The scale extent has been initailized with data extent from data via
+// `scale.setExtent` or `scale.unionExtentFromData`;
 export function niceScaleExtent(scale: Scale, model: AxisBaseModel) {
     const extentInfo = getScaleExtent(scale, model);
     const extent = extentInfo.extent;
@@ -427,3 +340,39 @@ export function shouldShowAllLabels(axis: Axis) {
         && getOptionCategoryInterval(axis.getLabelModel()) === 0;
 }
 
+// PEINDING: if there has been a stack dim (like '__\0ecstackresult'),
+// should we remove the original dim (like 'y') from the "coord dim"
+// in data? Thus the calling of `data.mapDimensionsAll(axis.dim)` will
+// not return the original dim and we do not to call `getDataDimensionsOnAxis`
+// manually in each coord sys.
+export function getDataDimensionsOnAxis(data: List, axisDim: string) {
+    // Remove duplicated dat dimensions caused by `getStackedDimension`.
+    const dataDimMap = {} as Dictionary<boolean>;
+    // Currently `mapDimensionsAll` will contian stack result dimension ('__\0ecstackresult').
+    zrUtil.each(data.mapDimensionsAll(axisDim), function (dataDim) {
+        // For example, the extent of the orginal dimension
+        // is [0.1, 0.5], the extent of the `stackResultDimension`
+        // is [7, 9], the final extent should NOT include [0.1, 0.5],
+        // because there is no graphic corresponding to [0.1, 0.5].
+        // See the case in `test/area-stack.html` `main1`, where area line
+        // stack needs `yAxis` not start from 0.
+        dataDimMap[getStackedDimension(data, dataDim)] = true;
+    });
+    return zrUtil.keys(dataDimMap);
+}
+
+export function unionAxisExtentFromData(dataExtent: number[], data: List, axisDim: string): void {
+    if (data) {
+        zrUtil.each(getDataDimensionsOnAxis(data, axisDim), function (dim) {
+            const seriesExtent = data.getApproximateExtent(dim);
+            seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]);
+            seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]);
+        });
+    }
+}
+
+export function parseAxisModelMinMax(scale: Scale, minMax: ScaleDataValue): number {
+    return minMax == null ? null
+        : zrUtil.eqNaN(minMax) ? NaN
+        : scale.parse(minMax);
+}
diff --git a/src/coord/axisModelCommonMixin.ts b/src/coord/axisModelCommonMixin.ts
index 1e45c5c..c1c458f 100644
--- a/src/coord/axisModelCommonMixin.ts
+++ b/src/coord/axisModelCommonMixin.ts
@@ -30,48 +30,9 @@ interface AxisModelCommonMixin<Opt extends AxisBaseOption> extends Pick<Model<Op
 
 class AxisModelCommonMixin<Opt extends AxisBaseOption> {
 
-    /**
-     * @return min value or 'dataMin' or null/undefined (means auto) or NaN
-     */
-    getMin(origin?: boolean): AxisBaseOption['min'] | number {
-        const option = this.option;
-        let min = (!origin && option.rangeStart != null)
-            ? option.rangeStart : option.min;
-
-        if (this.axis
-            && min != null
-            && min !== 'dataMin'
-            && typeof min !== 'function'
-            && !zrUtil.eqNaN(min)
-        ) {
-            min = this.axis.scale.parse(min);
-        }
-        return min;
-    }
-
-    /**
-     * @return max value or 'dataMax' or null/undefined (means auto) or NaN
-     */
-    getMax(origin?: boolean): AxisBaseOption['max'] | number {
-        const option = this.option;
-        let max = (!origin && option.rangeEnd != null)
-            ? option.rangeEnd : option.max;
-
-        if (this.axis
-            && max != null
-            && max !== 'dataMax'
-            && typeof max !== 'function'
-            && !zrUtil.eqNaN(max)
-        ) {
-            max = this.axis.scale.parse(max);
-        }
-        return max;
-    }
-
     getNeedCrossZero(): boolean {
         const option = this.option;
-        return (option.rangeStart != null || option.rangeEnd != null)
-            ? false : !option.scale;
+        return !option.scale;
     }
 
     /**
@@ -82,22 +43,6 @@ class AxisModelCommonMixin<Opt extends AxisBaseOption> {
         return;
     }
 
-    /**
-     * @param rangeStart Can only be finite number or null/undefined or NaN.
-     * @param rangeEnd Can only be finite number or null/undefined or NaN.
-     */
-    setRange(rangeStart: number, rangeEnd: number): void {
-        this.option.rangeStart = rangeStart;
-        this.option.rangeEnd = rangeEnd;
-    }
-
-    /**
-     * Reset range
-     */
-    resetRange(): void {
-        // rangeStart and rangeEnd is readonly.
-        this.option.rangeStart = this.option.rangeEnd = null;
-    }
 }
 
 export {AxisModelCommonMixin};
diff --git a/src/coord/cartesian/AxisModel.ts b/src/coord/cartesian/AxisModel.ts
index d8e4aee..3b6db87 100644
--- a/src/coord/cartesian/AxisModel.ts
+++ b/src/coord/cartesian/AxisModel.ts
@@ -44,21 +44,6 @@ class CartesianAxisModel extends ComponentModel<CartesianAxisOption>
 
     axis: Axis2D;
 
-    init(...args: any) {
-        super.init.apply(this, args);
-        this.resetRange();
-    }
-
-    mergeOption(...args: any) {
-        super.mergeOption.apply(this, args);
-        this.resetRange();
-    }
-
-    restoreData(...args: any) {
-        super.restoreData.apply(this, args);
-        this.resetRange();
-    }
-
     getCoordSysModel(): GridModel {
         return this.ecModel.queryComponents({
             mainType: 'grid',
diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts
index 0b0f2dd..4a0b54b 100644
--- a/src/coord/cartesian/Grid.ts
+++ b/src/coord/cartesian/Grid.ts
@@ -24,18 +24,18 @@
  */
 
 import {__DEV__} from '../../config';
-import {isObject, each, map, indexOf, retrieve, retrieve3} from 'zrender/src/core/util';
+import {isObject, each, indexOf, retrieve3} from 'zrender/src/core/util';
 import {getLayoutRect, LayoutRect} from '../../util/layout';
 import {
     createScaleByModel,
     ifAxisCrossZero,
     niceScaleExtent,
-    estimateLabelUnionRect
+    estimateLabelUnionRect,
+    getDataDimensionsOnAxis
 } from '../../coord/axisHelper';
 import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D';
 import Axis2D from './Axis2D';
 import CoordinateSystemManager from '../../CoordinateSystem';
-import {getStackedDimension} from '../../data/helper/dataStackHelper';
 import {ParsedModelFinder} from '../../util/model';
 
 // Depends on GridModel, AxisModel, which performs preprocess.
@@ -47,7 +47,7 @@ import { Dictionary } from 'zrender/src/core/types';
 import {CoordinateSystemMaster} from '../CoordinateSystem';
 import { ScaleDataValue } from '../../util/types';
 import List from '../../data/List';
-import SeriesModel from '../../model/Series';
+import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper';
 
 
 type Cartesian2DDimensionName = 'x' | 'y';
@@ -412,10 +412,10 @@ class Grid implements CoordinateSystemMaster {
             axis.scale.setExtent(Infinity, -Infinity);
         });
         ecModel.eachSeries(function (seriesModel) {
-            if (isCartesian2D(seriesModel)) {
-                const axesModels = findAxesModels(seriesModel);
-                const xAxisModel = axesModels[0];
-                const yAxisModel = axesModels[1];
+            if (isCartesian2DSeries(seriesModel)) {
+                const axesModelMap = findAxisModels(seriesModel);
+                const xAxisModel = axesModelMap.xAxisModel;
+                const yAxisModel = axesModelMap.yAxisModel;
 
                 if (!isAxisUsedInTheGrid(xAxisModel, gridModel)
                     || !isAxisUsedInTheGrid(yAxisModel, gridModel)
@@ -438,13 +438,8 @@ class Grid implements CoordinateSystemMaster {
         }, this);
 
         function unionExtent(data: List, axis: Axis2D): void {
-            each(data.mapDimensionsAll(axis.dim), function (dim) {
-                axis.scale.unionExtentFromData(
-                    // For example, the extent of the orginal dimension
-                    // is [0.1, 0.5], the extent of the `stackResultDimension`
-                    // is [7, 9], the final extent should not include [0.1, 0.5].
-                    data, getStackedDimension(data, dim)
-                );
+            each(getDataDimensionsOnAxis(data, axis.dim), function (dim) {
+                axis.scale.unionExtentFromData(data, dim);
             });
         }
     }
@@ -486,13 +481,13 @@ class Grid implements CoordinateSystemMaster {
 
         // Inject the coordinateSystems into seriesModel
         ecModel.eachSeries(function (seriesModel) {
-            if (!isCartesian2D(seriesModel)) {
+            if (!isCartesian2DSeries(seriesModel)) {
                 return;
             }
 
-            const axesModels = findAxesModels(seriesModel);
-            const xAxisModel = axesModels[0];
-            const yAxisModel = axesModels[1];
+            const axesModelMap = findAxisModels(seriesModel);
+            const xAxisModel = axesModelMap.xAxisModel;
+            const yAxisModel = axesModelMap.yAxisModel;
 
             const gridModel = xAxisModel.getCoordSysModel();
 
@@ -612,28 +607,6 @@ function updateAxisTransform(axis: Axis2D, coordBase: number) {
         };
 }
 
-const axesTypes = ['xAxis', 'yAxis'];
-function findAxesModels(seriesModel: SeriesModel): CartesianAxisModel[] {
-    return map(axesTypes, function (axisType) {
-        const axisModel = seriesModel.getReferringComponents(axisType)[0] as CartesianAxisModel;
-
-        if (__DEV__) {
-            if (!axisModel) {
-                throw new Error(axisType + ' "' + retrieve(
-                    seriesModel.get(axisType + 'Index' as any),
-                    seriesModel.get(axisType + 'Id' as any),
-                    0
-                ) + '" not found');
-            }
-        }
-        return axisModel;
-    });
-}
-
-function isCartesian2D(seriesModel: SeriesModel): boolean {
-    return seriesModel.get('coordinateSystem') === 'cartesian2d';
-}
-
 CoordinateSystemManager.register('cartesian2d', Grid);
 
 export default Grid;
diff --git a/src/coord/cartesian/cartesianAxisHelper.ts b/src/coord/cartesian/cartesianAxisHelper.ts
index db82726..f732f36 100644
--- a/src/coord/cartesian/cartesianAxisHelper.ts
+++ b/src/coord/cartesian/cartesianAxisHelper.ts
@@ -21,6 +21,8 @@
 import * as zrUtil from 'zrender/src/core/util';
 import GridModel from './GridModel';
 import CartesianAxisModel from './AxisModel';
+import SeriesModel from '../../model/Series';
+import { __DEV__ } from '../../config';
 
 interface CartesianAxisLayout {
     position: [number, number];
@@ -95,3 +97,36 @@ export function layout(
 
     return layout;
 }
+
+export function isCartesian2DSeries(seriesModel: SeriesModel): boolean {
+    return seriesModel.get('coordinateSystem') === 'cartesian2d';
+}
+
+export function findAxisModels(seriesModel: SeriesModel): {
+    xAxisModel: CartesianAxisModel;
+    yAxisModel: CartesianAxisModel;
+} {
+    const axisModelMap = {
+        xAxisModel: null,
+        yAxisModel: null
+    } as ReturnType<typeof findAxisModels>;
+    zrUtil.each(axisModelMap, function (v, key) {
+        const axisType = key.replace(/Model$/, '');
+        const axisModel = seriesModel.getReferringComponents(axisType)[0] as CartesianAxisModel;
+
+        if (__DEV__) {
+            if (!axisModel) {
+                throw new Error(axisType + ' "' + zrUtil.retrieve3(
+                    seriesModel.get(axisType + 'Index' as any),
+                    seriesModel.get(axisType + 'Id' as any),
+                    0
+                ) + '" not found');
+            }
+        }
+
+        axisModelMap[key] = axisModel;
+    });
+
+    return axisModelMap;
+}
+
diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts
index b214779..04789dd 100644
--- a/src/coord/polar/polarCreator.ts
+++ b/src/coord/polar/polarCreator.ts
@@ -25,10 +25,10 @@ import Polar from './Polar';
 import {parsePercent} from '../../util/number';
 import {
     createScaleByModel,
-    niceScaleExtent
+    niceScaleExtent,
+    getDataDimensionsOnAxis
 } from '../../coord/axisHelper';
 import CoordinateSystem from '../../CoordinateSystem';
-import {getStackedDimension} from '../../data/helper/dataStackHelper';
 
 import PolarModel from './PolarModel';
 import ExtensionAPI from '../../ExtensionAPI';
@@ -86,15 +86,11 @@ function updatePolarScale(this: Polar, ecModel: GlobalModel, api: ExtensionAPI)
     ecModel.eachSeries(function (seriesModel) {
         if (seriesModel.coordinateSystem === polar) {
             const data = seriesModel.getData();
-            zrUtil.each(data.mapDimensionsAll('radius'), function (dim) {
-                radiusAxis.scale.unionExtentFromData(
-                    data, getStackedDimension(data, dim)
-                );
+            zrUtil.each(getDataDimensionsOnAxis(data, 'radius'), function (dim) {
+                radiusAxis.scale.unionExtentFromData(data, dim);
             });
-            zrUtil.each(data.mapDimensionsAll('angle'), function (dim) {
-                angleAxis.scale.unionExtentFromData(
-                    data, getStackedDimension(data, dim)
-                );
+            zrUtil.each(getDataDimensionsOnAxis(data, 'angle'), function (dim) {
+                angleAxis.scale.unionExtentFromData(data, dim);
             });
         }
     });
diff --git a/src/coord/radar/Radar.ts b/src/coord/radar/Radar.ts
index 1a02f0e..52226c7 100644
--- a/src/coord/radar/Radar.ts
+++ b/src/coord/radar/Radar.ts
@@ -25,7 +25,8 @@ import IntervalScale from '../../scale/Interval';
 import * as numberUtil from '../../util/number';
 import {
     getScaleExtent,
-    niceScaleExtent
+    niceScaleExtent,
+    parseAxisModelMinMax
 } from '../axisHelper';
 import CoordinateSystemManager from '../../CoordinateSystem';
 import { CoordinateSystemMaster, CoordinateSystem } from '../CoordinateSystem';
@@ -192,8 +193,8 @@ class Radar implements CoordinateSystem, CoordinateSystemMaster {
 
             const axisModel = indicatorAxis.model;
             const scale = indicatorAxis.scale as IntervalScale;
-            const fixedMin = axisModel.getMin() as number;
-            const fixedMax = axisModel.getMax() as number;
+            const fixedMin = parseAxisModelMinMax(axisModel.axis.scale, axisModel.get('min', true) as ScaleDataValue);
+            const fixedMax = parseAxisModelMinMax(axisModel.axis.scale, axisModel.get('max', true) as ScaleDataValue);
             let interval = scale.getInterval();
 
             if (fixedMin != null && fixedMax != null) {
diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts
new file mode 100644
index 0000000..887757d
--- /dev/null
+++ b/src/coord/scaleRawExtentInfo.ts
@@ -0,0 +1,316 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See theICE 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 { __DEV__ } from '../config';
+import Scale from '../scale/Scale';
+import { AxisBaseModel } from './AxisBaseModel';
+import { parsePercent } from 'zrender/src/contain/text';
+import { parseAxisModelMinMax } from './axisHelper';
+import { AxisBaseOption } from './axisCommonTypes';
+
+
+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();
+
+        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 whethe 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 time.
+ * The code should not be duplicated either.
+ * So we cache the result in the scale instance, which will be recreated in the begining
+ * 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;
+}
+
diff --git a/src/data/List.ts b/src/data/List.ts
index 1b17cfa..3a1dda0 100644
--- a/src/data/List.ts
+++ b/src/data/List.ts
@@ -876,6 +876,14 @@ class List<
     }
 
     /**
+     * PENDING: In fact currently this function is only used to short-circuit
+     * the calling of `scale.unionExtentFromData` when data have been filtered by modules
+     * like "dataZoom". `scale.unionExtentFromData` is used to calculate data extent for series on
+     * an axis, but if a "axis related data filter module" is used, the extent of the axis have
+     * been fixed and no need to calling `scale.unionExtentFromData` actually.
+     * But if we add "custom data filter" in future, which is not "axis related", this method may
+     * be still needed.
+     *
      * Optimize for the scenario that data is filtered by a given extent.
      * Consider that if data amount is more than hundreds of thousand,
      * extent calculation will cost more than 10ms and the cache will
@@ -883,9 +891,15 @@ class List<
      */
     getApproximateExtent(dim: DimensionLoose): [number, number] {
         dim = this.getDimension(dim);
-        return this._approximateExtent[dim] || this.getDataExtent(dim /*, stack */);
+        return this._approximateExtent[dim] || this.getDataExtent(dim);
     }
 
+    /**
+     * Calculate extent on a filtered data might be time consuming.
+     * Approximate extent is only used for: calculte extent of filtered data outside.
+     * But if more than one modules do that work, that would be incorrect.
+     * We makes some assertion to check that.
+     */
     setApproximateExtent(extent: [number, number], dim: DimensionLoose): void {
         dim = this.getDimension(dim);
         this._approximateExtent[dim] = extent.slice() as [number, number];
diff --git a/src/echarts.ts b/src/echarts.ts
index b647380..18d3e68 100644
--- a/src/echarts.ts
+++ b/src/echarts.ts
@@ -88,9 +88,12 @@ export const dependencies = {
 
 const TEST_FRAME_REMAIN_TIME = 1;
 
-const PRIORITY_PROCESSOR_FILTER = 1000;
 const PRIORITY_PROCESSOR_SERIES_FILTER = 800;
 const PRIORITY_PROCESSOR_DATASTACK = 900;
+// "Data filter" will block the stream, so it should be
+// put at the begining of data processing.
+const PRIORITY_PROCESSOR_FILTER = 1000;
+const PRIORITY_PROCESSOR_DEFAULT = 2000;
 const PRIORITY_PROCESSOR_STATISTIC = 5000;
 
 const PRIORITY_VISUAL_LAYOUT = 1000;
@@ -2131,7 +2134,7 @@ export function registerProcessor(
     priority: number | StageHandler | StageHandlerOverallReset,
     processor?: StageHandler | StageHandlerOverallReset
 ): void {
-    normalizeRegister(dataProcessorFuncs, priority, processor, PRIORITY_PROCESSOR_FILTER);
+    normalizeRegister(dataProcessorFuncs, priority, processor, PRIORITY_PROCESSOR_DEFAULT);
 }
 
 /**
diff --git a/src/helper.ts b/src/helper.ts
index 85d4825..92622b4 100644
--- a/src/helper.ts
+++ b/src/helper.ts
@@ -102,8 +102,6 @@ export function createScale(dataExtent: number[], option: object | AxisBaseModel
  * `getMin(origin: boolean) => number`
  * `getMax(origin: boolean) => number`
  * `getNeedCrossZero() => boolean`
- * `setRange(start: number, end: number)`
- * `resetRange()`
  */
 export function mixinAxisModelCommonMethods(Model: Model) {
     zrUtil.mixin(Model, AxisModelCommonMixin);
diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts
index a5bf397..7cedc8a 100644
--- a/src/scale/Scale.ts
+++ b/src/scale/Scale.ts
@@ -22,6 +22,8 @@ import * as clazzUtil from '../util/clazz';
 import { Dictionary } from 'zrender/src/core/types';
 import List from '../data/List';
 import { DimensionName, ScaleDataValue, OptionDataValue } from '../util/types';
+import { ScaleRawExtentInfo } from '../coord/scaleRawExtentInfo';
+
 
 abstract class Scale {
 
@@ -33,6 +35,9 @@ abstract class Scale {
 
     private _isBlank: boolean;
 
+    // Inject
+    readonly rawExtentInfo: ScaleRawExtentInfo;
+
     constructor(setting?: Dictionary<any>) {
         this._setting = setting || {};
         this._extent = [Infinity, -Infinity];


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org